Skip to main content

apply/
apply.rs

1//! A minimal patch-apply tool using diffy's multi-file patch support.
2//!
3//! Usage:
4//!
5//! ```console
6//! apply <patch-file> [target-dir]
7//! ```
8//!
9//! Applies a git-format patch file to a target directory
10//! (defaults to the current directory).
11//!
12//! Assumes the default `a/` and `b/` path prefixes
13//! from `git diff` and GNU `diff -u`.
14
15use std::fs;
16use std::path::Path;
17use std::process::ExitCode;
18
19use diffy::apply_bytes;
20use diffy::binary::BinaryPatch;
21use diffy::patch_set::FileOperation;
22use diffy::patch_set::ParseOptions;
23use diffy::patch_set::PatchKind;
24use diffy::patch_set::PatchSet;
25
26fn main() -> ExitCode {
27    let args: Vec<String> = std::env::args().collect();
28    if args.len() < 2 || args.len() > 3 {
29        eprintln!("usage: {} <patch-file> [target-dir]", args[0]);
30        return ExitCode::FAILURE;
31    }
32    let patch_file = Path::new(&args[1]);
33    let target_dir = args.get(2).map_or_else(|| Path::new("."), |p| Path::new(p));
34
35    if let Err(e) = apply_patch_file(patch_file, target_dir) {
36        eprintln!("error: {e}");
37        return ExitCode::FAILURE;
38    }
39    ExitCode::SUCCESS
40}
41
42fn apply_patch_file(patch_file: &Path, dst: &Path) -> Result<(), Box<dyn std::error::Error>> {
43    let content = fs::read(patch_file)?;
44
45    let patches = PatchSet::parse_bytes(&content, ParseOptions::gitdiff());
46
47    for file_patch in patches {
48        let file_patch = file_patch?;
49        let operation = {
50            let op = file_patch.operation();
51            // Rename/Copy paths come from git headers without a/b prefix.
52            let strip = match op {
53                FileOperation::Rename { .. } | FileOperation::Copy { .. } => 0,
54                _ => 1,
55            };
56            op.strip_prefix(strip)
57        };
58
59        match operation {
60            FileOperation::Create(path) => {
61                let target = dst.join(path_from_bytes(&path)?);
62                let patched = match file_patch.patch() {
63                    PatchKind::Text(patch) => apply_bytes(&[], patch)?,
64                    PatchKind::Binary(BinaryPatch::Marker) => continue,
65                    PatchKind::Binary(patch) => patch.apply(&[])?,
66                };
67                create_parent_dirs(&target)?;
68                fs::write(&target, patched)?;
69                eprintln!("create {}", target.display());
70            }
71            FileOperation::Delete(path) => {
72                let target = dst.join(path_from_bytes(&path)?);
73                fs::remove_file(&target)?;
74                eprintln!("delete {}", target.display());
75            }
76            FileOperation::Modify { original, modified } => {
77                let src_path = dst.join(path_from_bytes(&original)?);
78                let dst_path = dst.join(path_from_bytes(&modified)?);
79                let patched = match file_patch.patch() {
80                    PatchKind::Text(patch) => {
81                        let base = fs::read(&src_path)?;
82                        apply_bytes(&base, patch)?
83                    }
84                    PatchKind::Binary(BinaryPatch::Marker) => continue,
85                    PatchKind::Binary(patch) => {
86                        let base = fs::read(&src_path)?;
87                        patch.apply(&base)?
88                    }
89                };
90                create_parent_dirs(&dst_path)?;
91                fs::write(&dst_path, patched)?;
92                if src_path != dst_path {
93                    fs::remove_file(&src_path)?;
94                    eprintln!("rename {} -> {}", src_path.display(), dst_path.display());
95                } else {
96                    eprintln!("modify {}", dst_path.display());
97                }
98            }
99            FileOperation::Rename { from, to } => {
100                let src_path = dst.join(path_from_bytes(&from)?);
101                let dst_path = dst.join(path_from_bytes(&to)?);
102                create_parent_dirs(&dst_path)?;
103                fs::rename(&src_path, &dst_path)?;
104                eprintln!("rename {} -> {}", src_path.display(), dst_path.display());
105            }
106            FileOperation::Copy { from, to } => {
107                let src_path = dst.join(path_from_bytes(&from)?);
108                let dst_path = dst.join(path_from_bytes(&to)?);
109                create_parent_dirs(&dst_path)?;
110                fs::copy(&src_path, &dst_path)?;
111                eprintln!("copy {} -> {}", src_path.display(), dst_path.display());
112            }
113        }
114    }
115
116    Ok(())
117}
118
119#[cfg(unix)]
120fn path_from_bytes(bytes: &[u8]) -> Result<&Path, Box<dyn std::error::Error>> {
121    use std::os::unix::ffi::OsStrExt;
122    Ok(Path::new(std::ffi::OsStr::from_bytes(bytes)))
123}
124
125#[cfg(not(unix))]
126fn path_from_bytes(bytes: &[u8]) -> Result<&Path, Box<dyn std::error::Error>> {
127    Ok(Path::new(std::str::from_utf8(bytes)?))
128}
129
130fn create_parent_dirs(path: &Path) -> std::io::Result<()> {
131    if let Some(parent) = path.parent() {
132        fs::create_dir_all(parent)?;
133    }
134    Ok(())
135}