update
This commit is contained in:
1 parent
d4fdc9890a
commit
5787b14880
22 files changed
+5766
-710
No files matched your search
+310
-12
@@ -1,11 +1,14 @@
|
||||
use super::HistEntry;
|
||||
use super::{BranchEntry, ContribEntry, HistEntry, TreeNode};
|
||||
use crate::status::format_age_pub;
|
||||
use git2::Repository;
|
||||
use git2::{BranchType, Repository};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
pub use crate::util::git::{git_run, git_run_ok};
|
||||
pub use crate::util::url::to_web_url;
|
||||
pub use crate::util::clipboard::copy_auto as copy_to_clipboard;
|
||||
|
||||
// ── commit history ────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn build_history(path: &std::path::Path, limit: usize) -> Vec<HistEntry> {
|
||||
let repo = match Repository::open(path) { Ok(r) => r, Err(_) => return vec![] };
|
||||
|
||||
@@ -52,11 +55,13 @@ pub fn build_history(path: &std::path::Path, limit: usize) -> Vec<HistEntry> {
|
||||
commits
|
||||
}
|
||||
|
||||
pub fn count_contributors(path: &std::path::Path) -> usize {
|
||||
pub fn total_commits(path: &std::path::Path) -> usize {
|
||||
std::process::Command::new("git")
|
||||
.arg("-C").arg(path).args(["shortlog", "-sn", "--all"])
|
||||
.arg("-C").arg(path).args(["rev-list", "--count", "HEAD"])
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).lines().count())
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.and_then(|s| s.trim().parse().ok())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -76,13 +81,7 @@ pub fn count_stashes(path: &std::path::Path) -> usize {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn count_local_branches(path: &std::path::Path) -> usize {
|
||||
std::process::Command::new("git")
|
||||
.arg("-C").arg(path).args(["branch"])
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).lines().count())
|
||||
.unwrap_or(1)
|
||||
}
|
||||
// ── files ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn list_all_files(path: &std::path::Path) -> Vec<String> {
|
||||
let out = std::process::Command::new("git")
|
||||
@@ -94,3 +93,302 @@ pub fn list_all_files(path: &std::path::Path) -> Vec<String> {
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines().map(|l| l.to_string()).collect()
|
||||
}
|
||||
|
||||
pub fn count_lines_of_code(path: &std::path::Path) -> usize {
|
||||
let out = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("git ls-files | head -5000 | xargs wc -l 2>/dev/null | tail -1")
|
||||
.current_dir(path)
|
||||
.output();
|
||||
match out {
|
||||
Ok(o) => {
|
||||
let s = String::from_utf8_lossy(&o.stdout);
|
||||
s.trim().split_whitespace().next().and_then(|n| n.parse().ok()).unwrap_or(0)
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn largest_files(path: &std::path::Path, n: usize) -> Vec<(String, usize)> {
|
||||
let out = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("git ls-files | head -5000 | xargs wc -l 2>/dev/null | sort -rn")
|
||||
.current_dir(path)
|
||||
.output();
|
||||
match out {
|
||||
Ok(o) => String::from_utf8_lossy(&o.stdout).lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
let mut parts = line.splitn(2, ' ');
|
||||
let count_str = parts.next()?.trim();
|
||||
let file = parts.next()?.trim();
|
||||
if file == "total" || file.is_empty() { return None; }
|
||||
let count = count_str.parse::<usize>().ok()?;
|
||||
Some((file.to_string(), count))
|
||||
})
|
||||
.take(n)
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// ── remote URL ────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn remote_url(path: &std::path::Path) -> Option<String> {
|
||||
let repo = Repository::open(path).ok()?;
|
||||
let remote = repo.find_remote("origin").ok()?;
|
||||
let url = remote.url()?;
|
||||
Some(to_web_url(url))
|
||||
}
|
||||
|
||||
// ── branches ─────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn list_branches(path: &std::path::Path) -> Vec<BranchEntry> {
|
||||
let repo = match Repository::open(path) { Ok(r) => r, Err(_) => return vec![] };
|
||||
let mut out = vec![];
|
||||
let branches = match repo.branches(Some(BranchType::Local)) {
|
||||
Ok(b) => b, Err(_) => return vec![],
|
||||
};
|
||||
for b in branches.flatten() {
|
||||
let (branch, _) = b;
|
||||
let name = match branch.name() {
|
||||
Ok(Some(n)) => n.to_string(),
|
||||
_ => continue,
|
||||
};
|
||||
let is_current = branch.is_head();
|
||||
|
||||
let upstream = branch.upstream().ok()
|
||||
.and_then(|u| u.name().ok().flatten().map(|n| n.to_string()));
|
||||
|
||||
let (ahead, behind) = if let Some(ref up) = upstream {
|
||||
if let (Ok(local_ref), Ok(up_ref)) = (
|
||||
repo.find_branch(&name, BranchType::Local).and_then(|b| b.into_reference().resolve()),
|
||||
repo.find_reference(&format!("refs/remotes/{}", up)).and_then(|r| r.resolve()),
|
||||
) {
|
||||
if let (Some(local_oid), Some(up_oid)) = (local_ref.target(), up_ref.target()) {
|
||||
repo.graph_ahead_behind(local_oid, up_oid).unwrap_or((0, 0))
|
||||
} else { (0, 0) }
|
||||
} else { (0, 0) }
|
||||
} else { (0, 0) };
|
||||
|
||||
let (last_commit_age, last_commit_msg) = branch.get().peel_to_commit().ok()
|
||||
.map(|c| (format_age_pub(c.time().seconds()), c.summary().unwrap_or("").chars().take(60).collect::<String>()))
|
||||
.unwrap_or_default();
|
||||
|
||||
out.push(BranchEntry { name, is_current, upstream, ahead, behind, last_commit_age, last_commit_msg });
|
||||
}
|
||||
out.sort_by(|a, b| b.is_current.cmp(&a.is_current).then(a.name.cmp(&b.name)));
|
||||
out
|
||||
}
|
||||
|
||||
// ── contributors ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn list_contributors(path: &std::path::Path) -> Vec<ContribEntry> {
|
||||
let out = std::process::Command::new("git")
|
||||
.arg("-C").arg(path).args(["shortlog", "-sn", "--all"])
|
||||
.output()
|
||||
.unwrap_or_else(|_| std::process::Output {
|
||||
status: std::process::ExitStatus::default(), stdout: vec![], stderr: vec![]
|
||||
});
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
let (count_str, name) = line.split_once('\t')?;
|
||||
let count = count_str.trim().parse::<usize>().ok()?;
|
||||
Some(ContribEntry { name: name.to_string(), count })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── commit activity (per period) ─────────────────────────────────────────────
|
||||
|
||||
fn git_activity_raw(path: &std::path::Path, since_days: Option<u64>, date_fmt: &str) -> HashMap<String, usize> {
|
||||
let since_arg = since_days.map(|d| format!("--since={} days ago", d));
|
||||
let mut cmd = std::process::Command::new("git");
|
||||
cmd.arg("-C").arg(path).arg("log");
|
||||
if let Some(ref s) = since_arg { cmd.arg(s); }
|
||||
cmd.args([&format!("--date=format:{}", date_fmt), "--pretty=format:%ad"]);
|
||||
let out = cmd.output().unwrap_or_else(|_| std::process::Output {
|
||||
status: std::process::ExitStatus::default(), stdout: vec![], stderr: vec![]
|
||||
});
|
||||
let mut counts = HashMap::new();
|
||||
for line in String::from_utf8_lossy(&out.stdout).lines() {
|
||||
let label = line.trim().to_string();
|
||||
if !label.is_empty() { *counts.entry(label).or_insert(0usize) += 1; }
|
||||
}
|
||||
counts
|
||||
}
|
||||
|
||||
pub fn activity_1d(path: &std::path::Path) -> Vec<(String, usize)> {
|
||||
let raw = git_activity_raw(path, Some(1), "%H");
|
||||
(0u32..24).map(|h| {
|
||||
let label = format!("{:02}", h);
|
||||
let count = raw.get(&label).copied().unwrap_or(0);
|
||||
(label, count)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn activity_7d(path: &std::path::Path) -> Vec<(String, usize)> {
|
||||
let raw = git_activity_raw(path, Some(7), "%Y-%m-%d");
|
||||
let now = chrono::Utc::now();
|
||||
(0i64..7).rev().map(|d| {
|
||||
let dt = now - chrono::Duration::days(d);
|
||||
let label = dt.format("%Y-%m-%d").to_string();
|
||||
let short = dt.format("%m-%d").to_string();
|
||||
let count = raw.get(&label).copied().unwrap_or(0);
|
||||
(short, count)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn activity_30d(path: &std::path::Path) -> Vec<(String, usize)> {
|
||||
let raw = git_activity_raw(path, Some(30), "%Y-%m-%d");
|
||||
let now = chrono::Utc::now();
|
||||
(0i64..30).rev().map(|d| {
|
||||
let dt = now - chrono::Duration::days(d);
|
||||
let label = dt.format("%Y-%m-%d").to_string();
|
||||
let short = dt.format("%d").to_string();
|
||||
let count = raw.get(&label).copied().unwrap_or(0);
|
||||
(short, count)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn activity_90d(path: &std::path::Path) -> Vec<(String, usize)> {
|
||||
let raw = git_activity_raw(path, Some(90), "%Y-%m-%d");
|
||||
let now = chrono::Utc::now();
|
||||
// Group into 7-day buckets (13 weeks)
|
||||
(0usize..13).rev().map(|w| {
|
||||
let end_d = (w * 7) as i64;
|
||||
let start_d = end_d + 7;
|
||||
let dt_end = now - chrono::Duration::days(end_d);
|
||||
let dt_start = now - chrono::Duration::days(start_d - 1);
|
||||
let label = format!("W{:02}", 13 - w);
|
||||
let short = dt_start.format("%m-%d").to_string();
|
||||
let count: usize = (0..7).map(|i| {
|
||||
let dt = now - chrono::Duration::days(end_d + i);
|
||||
let k = dt.format("%Y-%m-%d").to_string();
|
||||
raw.get(&k).copied().unwrap_or(0)
|
||||
}).sum();
|
||||
let _ = (label, dt_end);
|
||||
(short, count)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn activity_1y(path: &std::path::Path) -> Vec<(String, usize)> {
|
||||
let raw = git_activity_raw(path, Some(365), "%Y-%m");
|
||||
let now = chrono::Utc::now();
|
||||
(0i64..12).rev().map(|m| {
|
||||
let dt = now - chrono::Duration::days(m * 30);
|
||||
let label = dt.format("%Y-%m").to_string();
|
||||
let short = dt.format("%b").to_string();
|
||||
let count = raw.get(&label).copied().unwrap_or(0);
|
||||
(short, count)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn activity_all(path: &std::path::Path) -> Vec<(String, usize)> {
|
||||
let raw = git_activity_raw(path, None, "%Y");
|
||||
let mut result: Vec<(String, usize)> = raw.into_iter().collect();
|
||||
result.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
result
|
||||
}
|
||||
|
||||
// ── repo age ─────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn repo_first_commit_ts(path: &std::path::Path) -> Option<i64> {
|
||||
let repo = Repository::open(path).ok()?;
|
||||
let mut rw = repo.revwalk().ok()?;
|
||||
rw.push_head().ok()?;
|
||||
let oids: Vec<_> = rw.filter_map(|r| r.ok()).collect();
|
||||
let last_oid = oids.last()?;
|
||||
let commit = repo.find_commit(*last_oid).ok()?;
|
||||
Some(commit.time().seconds())
|
||||
}
|
||||
|
||||
// ── file tree ────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn build_tree(all_files: &[String]) -> Vec<TreeNode> {
|
||||
let mut dirs: BTreeSet<String> = BTreeSet::new();
|
||||
for f in all_files {
|
||||
let parts: Vec<&str> = f.split('/').collect();
|
||||
for i in 1..parts.len() {
|
||||
dirs.insert(parts[..i].join("/"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut dir_children: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
dir_children.entry(String::new()).or_default();
|
||||
|
||||
for dir in &dirs {
|
||||
let parent = dir.rfind('/').map(|i| &dir[..i]).unwrap_or("").to_string();
|
||||
dir_children.entry(parent).or_default().push(dir.clone());
|
||||
dir_children.entry(dir.clone()).or_default();
|
||||
}
|
||||
for f in all_files {
|
||||
let parent = f.rfind('/').map(|i| &f[..i]).unwrap_or("").to_string();
|
||||
dir_children.entry(parent).or_default().push(f.clone());
|
||||
}
|
||||
|
||||
for children in dir_children.values_mut() {
|
||||
children.sort_by(|a, b| {
|
||||
let a_is_dir = dirs.contains(a.as_str());
|
||||
let b_is_dir = dirs.contains(b.as_str());
|
||||
b_is_dir.cmp(&a_is_dir).then(a.cmp(b))
|
||||
});
|
||||
children.dedup();
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
flatten_tree("", &dir_children, &dirs, 0, &mut result);
|
||||
result
|
||||
}
|
||||
|
||||
fn flatten_tree(
|
||||
prefix: &str,
|
||||
dir_children: &BTreeMap<String, Vec<String>>,
|
||||
dirs: &BTreeSet<String>,
|
||||
depth: usize,
|
||||
out: &mut Vec<TreeNode>,
|
||||
) {
|
||||
let children = match dir_children.get(prefix) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
for child in children {
|
||||
let is_dir = dirs.contains(child.as_str());
|
||||
let name = child.rfind('/').map(|i| &child[i+1..]).unwrap_or(child).to_string();
|
||||
out.push(TreeNode { name, rel_path: child.clone(), is_dir, depth, expanded: true });
|
||||
if is_dir {
|
||||
flatten_tree(child, dir_children, dirs, depth + 1, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── file preview ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn read_file_preview(repo_path: &std::path::Path, rel: &str, max_lines: usize) -> Vec<String> {
|
||||
let full = repo_path.join(rel);
|
||||
let bytes = match std::fs::read(&full) {
|
||||
Ok(b) => b,
|
||||
Err(_) => return vec!["[cannot read file]".into()],
|
||||
};
|
||||
if bytes.iter().take(512).any(|&b| b == 0) {
|
||||
return vec!["[binary file]".into()];
|
||||
}
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
text.lines().take(max_lines).map(|l| l.to_string()).collect()
|
||||
}
|
||||
|
||||
// ── branch actions ───────────────────────────────────────────────────────────
|
||||
|
||||
pub fn checkout_branch(path: &std::path::Path, name: &str) -> bool {
|
||||
git_run_ok(path, &["checkout", name])
|
||||
}
|
||||
|
||||
pub fn delete_branch(path: &std::path::Path, name: &str) -> bool {
|
||||
git_run_ok(path, &["branch", "-d", name])
|
||||
}
|
||||
|
||||
pub fn push_branch(path: &std::path::Path, name: &str) -> bool {
|
||||
git_run_ok(path, &["push", "-u", "origin", name])
|
||||
}
|
||||
+1407
-118
File diff suppressed because it is too large.
Load diff
+1282
-305
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user