merge remote module into repo
This commit is contained in:
+59
-121
@@ -1,17 +1,14 @@
|
||||
use std::io::Write;
|
||||
use std::process::Stdio;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use anyhow::Result;
|
||||
use diffy::patch_set::{FileOperation, FilePatch, ParseOptions, PatchSet};
|
||||
use diffy::{Hunk, Line};
|
||||
|
||||
use crate::diff::{CommitDiff, DiffHunk, DiffLine, DiffLineKind, DiffStatus, FileDiff};
|
||||
use crate::history::FileCommit;
|
||||
use crate::repo::Repo;
|
||||
|
||||
pub struct PatchParser;
|
||||
|
||||
impl PatchParser {
|
||||
/// Splits an mbox series into the raw text of each message.
|
||||
pub fn split_patch_series(patch: &str) -> Vec<&str> {
|
||||
Self::envelopes(patch)
|
||||
.into_iter()
|
||||
@@ -19,44 +16,49 @@ impl PatchParser {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parses a git diff into file diffs, returning an empty diff without one.
|
||||
pub fn patch_diffs(patch: &str) -> Result<CommitDiff> {
|
||||
if !patch.lines().any(|line| line.starts_with("diff --git ")) {
|
||||
return Ok(CommitDiff { files: Vec::new() });
|
||||
return Ok(CommitDiff::new(Vec::new()));
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
|
||||
for file in PatchSet::parse(patch, ParseOptions::gitdiff()) {
|
||||
files.push(Self::file_diff(file?)?);
|
||||
files.push(Self::file_diff(file?));
|
||||
}
|
||||
|
||||
Ok(CommitDiff { files })
|
||||
Ok(CommitDiff::new(files))
|
||||
}
|
||||
|
||||
/// Lists the commits represented by the mbox messages that carry an id.
|
||||
pub fn patch_commits(patch: &str) -> Vec<FileCommit> {
|
||||
Self::envelopes(patch)
|
||||
.into_iter()
|
||||
.filter(|message| !message.id.is_empty())
|
||||
.map(|message| FileCommit {
|
||||
id: message.id.to_string(),
|
||||
summary: message
|
||||
.header("Subject")
|
||||
.map(Self::strip_patch_prefix)
|
||||
.unwrap_or_default(),
|
||||
description: None,
|
||||
author: message
|
||||
.header("From")
|
||||
.map(Self::name_from_address)
|
||||
.unwrap_or_default(),
|
||||
time: message
|
||||
.header("Date")
|
||||
.and_then(|value| gix::date::parse(value.trim(), None).ok())
|
||||
.map(|time| time.seconds)
|
||||
.unwrap_or(0),
|
||||
.map(|message| {
|
||||
FileCommit::new(
|
||||
message.id.to_string(),
|
||||
message
|
||||
.header("Subject")
|
||||
.map(Self::strip_patch_prefix)
|
||||
.unwrap_or_default(),
|
||||
None,
|
||||
message
|
||||
.header("From")
|
||||
.map(Self::name_from_address)
|
||||
.unwrap_or_default(),
|
||||
message
|
||||
.header("Date")
|
||||
.and_then(|value| gix::date::parse(value.trim(), None).ok())
|
||||
.map(|time| time.seconds)
|
||||
.unwrap_or(0),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Splits `patch` on `From <40-hex> <date>` envelopes, falling back to one message.
|
||||
fn envelopes(patch: &str) -> Vec<Envelope<'_>> {
|
||||
let mut messages: Vec<Envelope<'_>> = Vec::new();
|
||||
let mut current: Option<(usize, &str, Vec<&str>)> = None;
|
||||
@@ -74,11 +76,7 @@ impl PatchParser {
|
||||
|
||||
if is_envelope {
|
||||
if let Some((start, id, headers)) = current.take() {
|
||||
messages.push(Envelope {
|
||||
text: &patch[start..],
|
||||
id,
|
||||
headers,
|
||||
});
|
||||
messages.push(Envelope::new(&patch[start..], id, headers));
|
||||
}
|
||||
|
||||
let id = line
|
||||
@@ -104,24 +102,17 @@ impl PatchParser {
|
||||
}
|
||||
|
||||
if let Some((start, id, headers)) = current.take() {
|
||||
messages.push(Envelope {
|
||||
text: &patch[start..],
|
||||
id,
|
||||
headers,
|
||||
});
|
||||
messages.push(Envelope::new(&patch[start..], id, headers));
|
||||
}
|
||||
|
||||
if messages.is_empty() {
|
||||
messages.push(Envelope {
|
||||
text: patch,
|
||||
id: "",
|
||||
headers: Vec::new(),
|
||||
});
|
||||
messages.push(Envelope::new(patch, "", Vec::new()));
|
||||
}
|
||||
|
||||
messages
|
||||
}
|
||||
|
||||
/// Extracts the display name from a `From` header value.
|
||||
fn name_from_address(from: &str) -> String {
|
||||
match from.trim().find('<') {
|
||||
Some(ix) => from[..ix].trim().to_string(),
|
||||
@@ -129,7 +120,7 @@ impl PatchParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches `[PATCH]`, `[PATCH 1/2]`, `[RFC PATCH]`, etc.
|
||||
/// Strips mailing list prefixes like `[PATCH]` or `[RFC PATCH 1/2]`.
|
||||
fn strip_patch_prefix(subject: &str) -> String {
|
||||
let trimmed = subject.trim();
|
||||
let Some(rest) = trimmed.strip_prefix('[') else {
|
||||
@@ -145,7 +136,8 @@ impl PatchParser {
|
||||
}
|
||||
}
|
||||
|
||||
fn file_diff(file: FilePatch<'_, str>) -> Result<FileDiff> {
|
||||
/// Converts one parsed file patch, keeping rename and copy paths unstripped.
|
||||
fn file_diff(file: FilePatch<'_, str>) -> FileDiff {
|
||||
let stripped;
|
||||
let operation = match file.operation() {
|
||||
operation @ (FileOperation::Rename { .. } | FileOperation::Copy { .. }) => operation,
|
||||
@@ -192,17 +184,18 @@ impl PatchParser {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FileDiff {
|
||||
path: path.to_owned(),
|
||||
old_path: old_path.map(str::to_owned),
|
||||
FileDiff::new(
|
||||
path.to_owned(),
|
||||
old_path.map(str::to_owned),
|
||||
status,
|
||||
insertions,
|
||||
deletions,
|
||||
binary: patch.is_binary(),
|
||||
patch.is_binary(),
|
||||
hunks,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/// Converts a diffy hunk into a `DiffHunk` with per-line old and new numbers.
|
||||
fn hunk_diff(hunk: &Hunk<'_, str>) -> DiffHunk {
|
||||
let old_range = hunk.old_range();
|
||||
let new_range = hunk.new_range();
|
||||
@@ -237,37 +230,38 @@ impl PatchParser {
|
||||
}
|
||||
};
|
||||
|
||||
lines.push(DiffLine {
|
||||
kind,
|
||||
old: old_no,
|
||||
new: new_no,
|
||||
text: text
|
||||
.strip_suffix('\n')
|
||||
.unwrap_or(text)
|
||||
.strip_suffix('\r')
|
||||
.unwrap_or(text)
|
||||
.to_owned(),
|
||||
});
|
||||
let text = text
|
||||
.strip_suffix('\n')
|
||||
.unwrap_or(text)
|
||||
.strip_suffix('\r')
|
||||
.unwrap_or(text);
|
||||
|
||||
lines.push(DiffLine::new(kind, old_no, new_no, text.to_owned()));
|
||||
}
|
||||
|
||||
DiffHunk {
|
||||
old_start: old_range.start() as u32,
|
||||
old_lines: old_range.len() as u32,
|
||||
new_start: new_range.start() as u32,
|
||||
new_lines: new_range.len() as u32,
|
||||
DiffHunk::new(
|
||||
old_range.start() as u32,
|
||||
old_range.len() as u32,
|
||||
new_range.start() as u32,
|
||||
new_range.len() as u32,
|
||||
lines,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A `git format-patch` mbox message, split on its `From <40-hex> <date>` envelope.
|
||||
struct Envelope<'a> {
|
||||
text: &'a str,
|
||||
id: &'a str,
|
||||
headers: Vec<&'a str>,
|
||||
}
|
||||
|
||||
impl Envelope<'_> {
|
||||
impl<'a> Envelope<'a> {
|
||||
/// Creates an envelope from its text, commit id, and header lines.
|
||||
fn new(text: &'a str, id: &'a str, headers: Vec<&'a str>) -> Self {
|
||||
Self { text, id, headers }
|
||||
}
|
||||
|
||||
/// Returns the first header line matching `name: `.
|
||||
fn header(&self, name: &str) -> Option<&str> {
|
||||
let prefix = format!("{name}: ");
|
||||
self.headers
|
||||
@@ -276,59 +270,3 @@ impl Envelope<'_> {
|
||||
.map(|line| &line[prefix.len()..])
|
||||
}
|
||||
}
|
||||
|
||||
impl Repo {
|
||||
pub fn apply_patch(&self, patch: &str) -> Result<()> {
|
||||
let workdir = self
|
||||
.inner
|
||||
.workdir()
|
||||
.context("repository has no worktree")?
|
||||
.to_path_buf();
|
||||
|
||||
let mut child = Repo::git_command(&workdir)
|
||||
.args(["am"])
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("failed to spawn `git am`")?;
|
||||
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.context("git am has no stdin pipe")?
|
||||
.write_all(patch.as_bytes())?;
|
||||
|
||||
let output = child.wait_with_output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("git am failed: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Fails when the range has no commits. The mbox is returned untrimmed;
|
||||
// trailing newlines are part of the format.
|
||||
pub fn format_patch_between(&self, base: &str, tip: &str) -> Result<String> {
|
||||
let output = Repo::run_git(
|
||||
self.workdir_or_dot(),
|
||||
&["format-patch", "--stdout", &format!("{base}..{tip}")],
|
||||
"git format-patch",
|
||||
)?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"git format-patch failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
);
|
||||
}
|
||||
|
||||
let patch = String::from_utf8_lossy(&output.stdout).into_owned();
|
||||
|
||||
if patch.trim().is_empty() {
|
||||
bail!("no commits between {base} and {tip}");
|
||||
}
|
||||
|
||||
Ok(patch)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user