Skip to content
This repository was archived by the owner on Oct 19, 2026. It is now read-only.

Commit 64e0526

Browse files
huizmgenedna
andauthored
Implement argument --[no-]decorate[=...] for libra log, show all reference info (#1504)
* feat(log): add decoration option arguments * feat(log): get tag refs for each commit * refactor(libra): move to internal * Revert "refactor(libra): move to internal" This reverts commit da9e455. * feat(libra/log): also retrieve tags of commits * feat(libra/log): display full ref info * fix(libra/log): pass clippy tests --------- Co-authored-by: Quanyi Ma <eli@patch.sh>
1 parent 352ad54 commit 64e0526

1 file changed

Lines changed: 221 additions & 66 deletions

File tree

‎libra/src/command/log.rs‎

Lines changed: 221 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::path::PathBuf;
44

55
use crate::command::load_object;
66
use crate::internal::branch::Branch;
7+
use crate::internal::config::Config;
78
use crate::internal::head::Head;
89
use clap::Parser;
910
use colored::Colorize;
@@ -32,12 +33,85 @@ pub struct LogArgs {
3233
/// Show diffs for each commit (like git -p)
3334
#[clap(short = 'p', long = "patch")]
3435
pub patch: bool,
36+
/// Print out ref names of any commits that are shown
37+
#[clap(
38+
long,
39+
default_missing_value = "short",
40+
require_equals = true,
41+
num_args = 0..=1,
42+
)]
43+
pub decorate: Option<String>,
44+
/// Do not print out ref names of any commits that are shown
45+
#[clap(long)]
46+
pub no_decorate: bool,
3547

3648
/// Files to limit diff output (only used with -p)
3749
#[clap(requires = "patch", value_name = "PATHS")]
3850
pathspec: Vec<String>,
3951
}
4052

53+
#[derive(PartialEq)]
54+
enum DecorateOptions {
55+
No,
56+
Short,
57+
Full,
58+
}
59+
60+
fn str_to_decorate_option(s: &str) -> Result<DecorateOptions, String> {
61+
match s {
62+
"no" => Ok(DecorateOptions::No),
63+
"short" => Ok(DecorateOptions::Short),
64+
"full" => Ok(DecorateOptions::Full),
65+
"auto" => {
66+
if std::io::IsTerminal::is_terminal(&std::io::stdout()) {
67+
Ok(DecorateOptions::Short)
68+
} else {
69+
Ok(DecorateOptions::No)
70+
}
71+
}
72+
_ => Err(s.to_owned()),
73+
}
74+
}
75+
76+
async fn determine_decorate_option(args: &LogArgs) -> Result<DecorateOptions, String> {
77+
let arg_deco = args
78+
.decorate
79+
.as_ref()
80+
.map(|s| str_to_decorate_option(s))
81+
.transpose()?;
82+
83+
match arg_deco {
84+
Some(a) => {
85+
if args.no_decorate {
86+
let args_os = std::env::args_os().peekable();
87+
for arg in args_os {
88+
if arg == "--no-decorate" {
89+
return Ok(a);
90+
} else if arg.to_str().unwrap_or_default().starts_with("--decorate") {
91+
return Ok(DecorateOptions::No);
92+
};
93+
}
94+
} else {
95+
return Ok(a);
96+
}
97+
}
98+
None => {
99+
if args.no_decorate {
100+
return Ok(DecorateOptions::No);
101+
}
102+
}
103+
};
104+
105+
if let Some(config_deco) = Config::get("log", None, "decorate")
106+
.await
107+
.and_then(|s| str_to_decorate_option(&s).ok())
108+
{
109+
Ok(config_deco)
110+
} else {
111+
str_to_decorate_option("auto")
112+
}
113+
}
114+
41115
/// Get all reachable commits from the given commit hash
42116
/// **didn't consider the order of the commits**
43117
pub async fn get_reachable_commits(commit_hash: String) -> Vec<Commit> {
@@ -65,7 +139,25 @@ pub async fn get_reachable_commits(commit_hash: String) -> Vec<Commit> {
65139
reachable_commits
66140
}
67141

142+
// Ordered as they should appear in log
143+
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
144+
enum ReferenceKind {
145+
Tag, // decorate color = yellow
146+
Remote, // red
147+
Local, // green
148+
}
149+
150+
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
151+
struct Reference {
152+
kind: ReferenceKind,
153+
name: String,
154+
}
155+
68156
pub async fn execute(args: LogArgs) {
157+
let decorate_option = determine_decorate_option(&args)
158+
.await
159+
.expect("fatal: invalid --decorate option");
160+
69161
#[cfg(unix)]
70162
let mut process = Command::new("less") // create a pipe to less
71163
.arg("-R") // raw control characters
@@ -77,20 +169,25 @@ pub async fn execute(args: LogArgs) {
77169

78170
let head = Head::current().await;
79171
// check if the current branch has any commits
80-
if let Head::Branch(branch_name) = head.to_owned() {
81-
let branch = Branch::find_branch(&branch_name, None).await;
172+
let branch_name = if let Head::Branch(n) = head.to_owned() {
173+
Some(n)
174+
} else {
175+
None
176+
};
177+
if let Some(n) = &branch_name {
178+
let branch = Branch::find_branch(n, None).await;
82179
if branch.is_none() {
83-
panic!("fatal: your current branch '{branch_name}' does not have any commits yet ");
84-
}
85-
}
180+
panic!("fatal: your current branch '{n}' does not have any commits yet ");
181+
};
182+
};
86183

87184
let commit_hash = Head::current_commit().await.unwrap().to_string();
88185

89186
let mut reachable_commits = get_reachable_commits(commit_hash.clone()).await;
90187
// default sort with signature time
91188
reachable_commits.sort_by(|a, b| b.committer.timestamp.cmp(&a.committer.timestamp));
92189

93-
let branch_commits = create_branch_commits_map().await;
190+
let ref_commits = create_reference_commit_map().await;
94191

95192
let max_output_number = min(args.number.unwrap_or(usize::MAX), reachable_commits.len());
96193
let mut output_number = 0;
@@ -100,66 +197,105 @@ pub async fn execute(args: LogArgs) {
100197
}
101198
output_number += 1;
102199

103-
let branches = branch_commits.get(&commit.id).cloned().unwrap_or_default();
200+
let ref_msg = if decorate_option != DecorateOptions::No {
201+
let mut ref_msgs: Vec<String> = vec![];
202+
if output_number == 1 {
203+
ref_msgs.push(if let Some(b_name) = &branch_name {
204+
format!(
205+
"{} -> {}{}",
206+
"HEAD".cyan(),
207+
(if decorate_option == DecorateOptions::Full {
208+
"refs/heads/"
209+
} else {
210+
""
211+
})
212+
.green(),
213+
b_name.green()
214+
)
215+
} else {
216+
"HEAD".cyan().to_string()
217+
});
218+
};
219+
220+
let mut refs = ref_commits.get(&commit.id).cloned().unwrap_or_default();
221+
refs.sort();
222+
223+
ref_msgs.append(
224+
&mut refs
225+
.iter()
226+
.filter_map(|r| {
227+
if r.kind == ReferenceKind::Local && Some(r.name.to_owned()) == branch_name
228+
{
229+
None
230+
} else {
231+
Some(match r.kind {
232+
ReferenceKind::Tag => format!(
233+
"tag: {}{}",
234+
if decorate_option == DecorateOptions::Full {
235+
"refs/tags/"
236+
} else {
237+
""
238+
},
239+
r.name
240+
)
241+
.yellow()
242+
.to_string(),
243+
ReferenceKind::Remote => format!(
244+
"{}{}",
245+
if decorate_option == DecorateOptions::Full {
246+
"refs/remotes/"
247+
} else {
248+
""
249+
},
250+
r.name
251+
)
252+
.red()
253+
.to_string(),
254+
ReferenceKind::Local => format!(
255+
"{}{}",
256+
if decorate_option == DecorateOptions::Full {
257+
"refs/heads/"
258+
} else {
259+
""
260+
},
261+
r.name
262+
)
263+
.green()
264+
.to_string(),
265+
})
266+
}
267+
})
268+
.collect(),
269+
);
270+
ref_msgs.join(", ")
271+
} else {
272+
String::new()
273+
};
104274

105275
// prepare pathspecs for diff if needed
106276
let paths: Vec<PathBuf> = args.pathspec.iter().map(util::to_workdir_path).collect();
107277

108278
let message = if args.oneline {
109-
// Oneline format: <short_hash> <commit_message_first_line>
279+
// Oneline format: <short_hash> <refs> <commit_message_first_line>
110280
let short_hash = &commit.id.to_string()[..7];
111281
let (msg, _) = parse_commit_msg(&commit.message);
112-
if !branches.is_empty() {
113-
let branch_info = format!(" ({})", branches.join(", "));
114-
format!(
115-
"{} {}{}",
116-
short_hash.yellow().bold(),
117-
msg,
118-
branch_info.green()
119-
)
282+
if !ref_msg.is_empty() {
283+
format!("{} ({}) {}", short_hash.yellow(), ref_msg, msg)
120284
} else {
121285
format!("{} {}", short_hash.yellow(), msg)
122286
}
123287
} else {
124288
// Default detailed format
125-
let mut message = format!(
126-
"{} {}",
127-
"commit".yellow(),
128-
if !branches.is_empty() {
129-
commit.id.to_string().yellow().bold()
130-
} else {
131-
commit.id.to_string().yellow()
132-
}
133-
);
134-
135-
// Show HEAD and branch info
136-
if output_number == 1 {
137-
// For the first commit (HEAD), show HEAD info and all branches
138-
let mut refs = vec![];
139-
let current_branch = if let Head::Branch(name) = head.to_owned() {
140-
refs.push(format!("{} -> {}", "HEAD".blue(), name.green()));
141-
Some(name)
142-
} else {
143-
refs.push("HEAD".blue().to_string());
144-
None
145-
};
146-
147-
// Add other branches pointing to this commit (excluding current branch)
148-
let other_branches: Vec<String> = branches
149-
.iter()
150-
.filter(|&b| current_branch.as_ref() != Some(b))
151-
.map(|b| b.green().to_string())
152-
.collect();
153-
154-
refs.extend(other_branches);
155-
156-
let ref_info = format!(" ({})", refs.join(", "));
157-
message = format!("{message}{ref_info}");
158-
} else if !branches.is_empty() {
159-
// Show branch info for other commits that are branch heads
160-
let branch_info = format!(" ({})", branches.join(", "));
161-
message = format!("{}{}", message, branch_info.green());
162-
}
289+
let mut message = if !ref_msg.is_empty() {
290+
format!(
291+
"{} {} ({})",
292+
"commit".yellow(),
293+
commit.id.to_string().yellow(),
294+
ref_msg
295+
)
296+
} else {
297+
format!("{} {}", "commit".yellow(), commit.id.to_string().yellow())
298+
};
163299

164300
message.push_str(&format!("\nAuthor: {}", commit.author));
165301
let (msg, _) = parse_commit_msg(&commit.message);
@@ -192,24 +328,43 @@ pub async fn execute(args: LogArgs) {
192328
}
193329
}
194330

195-
/// Create a map of commit hashes to branch names
196-
async fn create_branch_commits_map() -> HashMap<SHA1, Vec<String>> {
197-
let all_branches = Branch::list_branches(None).await;
198-
let mut commit_to_branches: HashMap<SHA1, Vec<String>> = HashMap::new();
331+
async fn create_reference_commit_map() -> HashMap<SHA1, Vec<Reference>> {
332+
let mut commit_to_refs: HashMap<SHA1, Vec<Reference>> = HashMap::new();
199333

334+
let all_branches = Branch::list_branches(None).await;
200335
for branch in all_branches {
201-
let branch_name = match &branch.remote {
202-
Some(remote) => format!("{}/{}", remote, branch.name),
203-
None => branch.name,
204-
};
205-
206-
commit_to_branches
336+
commit_to_refs
207337
.entry(branch.commit)
208338
.or_default()
209-
.push(branch_name);
339+
.push(match &branch.remote {
340+
Some(remote) => Reference {
341+
name: format!("{}/{}", remote, branch.name),
342+
kind: ReferenceKind::Remote,
343+
},
344+
None => Reference {
345+
name: branch.name,
346+
kind: ReferenceKind::Local,
347+
},
348+
});
349+
}
350+
351+
let all_tags = crate::internal::tag::list().await.expect("fatal: ");
352+
for tag in all_tags {
353+
let commit_id = match tag.object {
354+
crate::internal::tag::TagObject::Commit(c) => c.id,
355+
crate::internal::tag::TagObject::Tag(t) => t.object_hash,
356+
_ => continue,
357+
};
358+
commit_to_refs
359+
.entry(commit_id)
360+
.or_default()
361+
.push(Reference {
362+
name: tag.name,
363+
kind: ReferenceKind::Tag,
364+
});
210365
}
211366

212-
commit_to_branches
367+
commit_to_refs
213368
}
214369

215370
/// Generate unified diff between commit and its first parent (or empty tree)

0 commit comments

Comments
 (0)