patchy/commands/
gen_patch.rs

1use 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        // After "--", each argument is interpreted literally. This way, we can e.g. use filenames that are named exactly the same as flags
39        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            // Do not consider flags as arguments
52            continue;
53        }
54
55        // Only merge commits can have 2 or more parents
56        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        // 1. if the user provides a custom filename for the patch file, use that
93        // 2. otherwise use the commit message
94        // 3. if all fails use the commit hash
95        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        // Paths are UTF-8 encoded. If we cannot convert to UTF-8 that means it is not a valid path
107        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}