patchy/commands/
gen_patch.rs1use std::{fs, process};
2
3use crate::CONFIG_ROOT;
4use crate::{
5 commands::help,
6 fail,
7 flags::{is_valid_flag, Flag},
8 git_commands::{is_valid_branch_name, GIT, GIT_ROOT},
9 success,
10 types::CommandArgs,
11 utils::normalize_commit_msg,
12};
13
14use super::help::{HELP_FLAG, VERSION_FLAG};
15
16pub static GEN_PATCH_NAME_FLAG: Flag<'static> = Flag {
17 short: "-n=",
18 long: "--patch-filename=",
19 description: "Choose filename for the patch",
20};
21
22pub static GEN_PATCH_FLAGS: &[&Flag<'static>; 3] =
23 &[&GEN_PATCH_NAME_FLAG, &HELP_FLAG, &VERSION_FLAG];
24
25pub fn gen_patch(args: &CommandArgs) -> anyhow::Result<()> {
26 if args.is_empty() {
27 fail!("You haven't specified any commit hashes");
28 help(Some("gen-patch"))?;
29 }
30 let mut args = args.iter().peekable();
31 let mut commit_hashes_with_maybe_custom_patch_filenames = vec![];
32
33 let config_path = GIT_ROOT.join(CONFIG_ROOT);
34
35 let mut no_more_flags = false;
36
37 while let Some(arg) = args.next() {
38 if arg == "--" {
40 no_more_flags = true;
41 continue;
42 };
43
44 if arg.starts_with('-') && !no_more_flags {
45 if !is_valid_flag(arg, GEN_PATCH_FLAGS) {
46 fail!("Invalid flag: {arg}");
47 let _ = help(Some("gen-patch"));
48 process::exit(1);
49 }
50
51 continue;
53 }
54
55 let is_merge_commit = GIT(&["rev-parse", &format!("{}^2", arg)]).is_ok();
57
58 if is_merge_commit {
59 fail!(
60 "Commit {} is a merge commit, which cannot be turned into a .patch file",
61 arg
62 );
63
64 continue;
65 }
66
67 let next_arg = args.peek();
68 let maybe_custom_patch_filename: Option<String> = next_arg.and_then(|next_arg| {
69 GEN_PATCH_NAME_FLAG
70 .extract_from_arg(next_arg)
71 .filter(|branch_name| is_valid_branch_name(branch_name))
72 });
73
74 if maybe_custom_patch_filename.is_some() {
75 args.next();
76 };
77
78 commit_hashes_with_maybe_custom_patch_filenames.push((arg, maybe_custom_patch_filename));
79 }
80
81 if !config_path.exists() {
82 success!(
83 "Config directory {} does not exist, creating it...",
84 config_path.to_string_lossy()
85 );
86 fs::create_dir_all(&config_path)?;
87 }
88
89 for (patch_commit_hash, maybe_custom_patch_name) in
90 commit_hashes_with_maybe_custom_patch_filenames
91 {
92 let patch_filename = maybe_custom_patch_name.unwrap_or_else(|| {
96 GIT(&["log", "--format=%B", "--max-count=1", patch_commit_hash]).map_or_else(
97 |_| patch_commit_hash.to_owned(),
98 |commit_msg| normalize_commit_msg(&commit_msg),
99 )
100 });
101
102 let patch_filename = format!("{patch_filename}.patch");
103
104 let patch_file_path = config_path.join(&patch_filename);
105
106 let Some(patch_file_path_str) = patch_file_path.as_os_str().to_str() else {
108 fail!("Not a valid path: {patch_file_path:?}");
109 continue;
110 };
111
112 if let Err(err) = GIT(&[
113 "format-patch",
114 "-1",
115 patch_commit_hash,
116 "--output",
117 patch_file_path_str,
118 ]) {
119 fail!(
120 "Could not get patch output for patch {}\n{err}",
121 patch_commit_hash
122 );
123 continue;
124 };
125
126 success!(
127 "Created patch file at {}",
128 patch_file_path.to_string_lossy()
129 );
130 }
131
132 Ok(())
133}