1use anyhow::{Result, bail};
2use std::path::Path;
3use std::process::Command;
4
5mod commits;
6mod detect;
7mod github_api;
8mod mailmap;
9mod remote;
10mod semver;
11mod snapshot_sde;
12mod status;
13mod tags;
14pub mod worktree;
15
16#[cfg(test)]
17mod tests;
18
19pub use commits::{
20 Commit, CommitWithFiles, CommitterIdentity, SHORT_COMMIT_LEN, add_path_in,
21 branches_containing_sha_in, commit_in, commit_subject_in, commits_between_in,
22 commits_with_subjects_in, count_commits_since_last_tag_in, get_all_commits, get_all_commits_in,
23 get_all_commits_paths, get_all_commits_paths_in, get_all_commits_paths_with_files_in,
24 get_commit_messages_between, get_commit_messages_between_in, get_commit_messages_between_path,
25 get_commit_messages_between_path_in, get_commits_between, get_commits_between_in,
26 get_commits_between_paths, get_commits_between_paths_in,
27 get_commits_between_paths_with_files_in, get_commits_reachable_paths_in,
28 get_commits_reachable_paths_with_files_in, get_current_branch, get_current_branch_in,
29 get_current_branch_in_with_env, get_head_commit, get_head_commit_in, get_last_commit_messages,
30 get_last_commit_messages_in, get_last_commit_messages_path, get_last_commit_messages_path_in,
31 get_short_commit, get_short_commit_in, has_changes_since, has_changes_since_in,
32 has_commits_since_tag, has_commits_since_tag_in, head_commit_hash_in, head_commit_timestamp_in,
33 is_branchlike, log_subjects_for_range, parse_commit_output, parse_commit_output_with_files,
34 paths_changed_since_tag, paths_changed_since_tag_in, push_branch_in, reset_hard_in,
35 resolve_rollback_identity, rev_parse_in, rev_verify_commit_in, revert_commit_in,
36 short_commit_str, stage_and_commit, stage_and_commit_in,
37};
38pub use detect::{GitInfo, detect_git_info, detect_git_info_in};
39pub use github_api::{
40 commit_author_login, commit_author_login_with_binary, create_tag_via_github_api,
41 create_tag_via_github_api_in, gh_api_get, gh_api_get_paginated,
42 gh_api_get_paginated_with_binary, gh_api_get_with_binary, resolve_github_token,
43 resolve_github_token_with_env,
44};
45pub use mailmap::canonical_author_email_in;
46pub use remote::{
47 detect_github_repo, detect_github_repo_in, detect_owner_repo, detect_owner_repo_in,
48 detect_remote_web_base_in, has_remote_in, parse_github_remote, parse_remote_owner_repo,
49 parse_remote_web_base,
50};
51pub use semver::{SemVer, parse_semver, parse_semver_tag};
52pub use snapshot_sde::resolve_snapshot_sde;
53pub use status::{
54 check_git_available, git_status_porcelain, git_status_porcelain_in, is_git_dirty,
55 is_git_dirty_in, is_git_repo, is_git_repo_in, is_shallow_clone, is_shallow_clone_in,
56 list_tracked_files_in, local_git_user_email, local_git_user_email_in, local_git_user_name,
57 local_git_user_name_in,
58};
59pub use tags::{
60 AtomicPushSpec, create_and_push_tag, create_and_push_tag_in, create_tag_local_only,
61 delete_local_tag_in, delete_remote_tag_in, extract_tag_prefix, find_latest_tag_matching,
62 find_latest_tag_matching_in, find_latest_tag_matching_with_prefix,
63 find_latest_tag_matching_with_prefix_in, find_previous_tag, find_previous_tag_in,
64 find_previous_tag_with_prefix, find_previous_tag_with_prefix_in, get_all_semver_tags,
65 get_all_semver_tags_in, get_branch_semver_tags, get_branch_semver_tags_in, get_first_commit,
66 get_first_commit_in, get_tags_at_head, get_tags_at_head_in, get_tags_at_sha_in,
67 has_version_placeholder, head_is_at_tag, list_tags_with_prefix, push_branch_and_tags_atomic_in,
68 render_ignore_patterns, strip_monorepo_prefix, tag_points_at_head, tag_points_at_head_in,
69};
70pub use worktree::Worktree;
71
72pub(crate) fn git_output_in(cwd: &Path, args: &[&str]) -> Result<String> {
86 let output = Command::new("git")
93 .current_dir(cwd)
94 .args(args)
95 .env("GIT_TERMINAL_PROMPT", "0")
96 .env("LC_ALL", "C")
97 .output()?;
98 if !output.status.success() {
99 let stderr_raw = String::from_utf8_lossy(&output.stderr);
100 let stderr_trim = stderr_raw.trim();
101 let detail = if stderr_trim.is_empty() {
106 String::from_utf8_lossy(&output.stdout).trim().to_string()
107 } else {
108 stderr_trim.to_string()
109 };
110 let raw = format!("git {} failed: {}", args.join(" "), detail);
111 bail!("{}", crate::redact::redact_process_env(&raw));
112 }
113 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
114}