1
0
mirror of https://github.com/skeeto/w64devkit.git synced 2026-10-24 17:39:18 +03:00
Files
w64devkit/src/quilt.cpp
Christopher Wellons c4ee256cc2 Introduce quilt command (Quilt.cpp)
This is a primitive source control system for w64devkit, a C++ clone of
a classic, old-school tool still in some use today.
2026-04-05 22:03:30 -04:00

9535 lines
331 KiB
C++

// quilt.cpp — single-file amalgamation (Windows platform)
// $ c++ -std=c++20 -o quilt.exe quilt.cpp -lshell32
// $ cl /std:c++20 /EHsc quilt.cpp shell32.lib
// This is free and unencumbered software released into the public domain.
#define QUILT_VERSION "0.69"
// === src/quilt.hpp ===
// This is free and unencumbered software released into the public domain.
#include <cassert>
#include <charconv>
#include <format>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <algorithm>
#include <functional>
#include <iterator>
#include <map>
#include <set>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
// Checked integer cast: asserts value is representable in the target type.
template<typename To, typename From>
constexpr To checked_cast(From value) {
static_assert(std::is_integral_v<To> && std::is_integral_v<From>);
assert(std::in_range<To>(value));
return static_cast<To>(value);
}
// Parse a decimal integer from a string_view. Returns 0 on failure.
inline ptrdiff_t parse_int(std::string_view s) {
ptrdiff_t val = 0;
std::from_chars(s.data(), s.data() + s.size(), val);
return val;
}
// find/rfind wrappers returning ptrdiff_t (-1 for not-found).
inline ptrdiff_t str_find(std::string_view s, char c, ptrdiff_t pos = 0) {
auto r = s.find(c, checked_cast<size_t>(pos));
return r == std::string_view::npos ? ptrdiff_t{-1} : static_cast<ptrdiff_t>(r);
}
inline ptrdiff_t str_find(std::string_view s, std::string_view needle, ptrdiff_t pos = 0) {
auto r = s.find(needle, checked_cast<size_t>(pos));
return r == std::string_view::npos ? ptrdiff_t{-1} : static_cast<ptrdiff_t>(r);
}
inline ptrdiff_t str_rfind(std::string_view s, char c) {
auto r = s.rfind(c);
return r == std::string_view::npos ? ptrdiff_t{-1} : static_cast<ptrdiff_t>(r);
}
// Quilt .pc/ directory state
struct QuiltState {
std::string work_dir; // project root
std::string patches_dir; // typically "patches"
std::string pc_dir; // typically ".pc"
std::string series_file; // "patches/series"
std::string subdir; // cwd relative to work_dir (empty at root)
bool series_file_exists = false;
std::vector<std::string> series; // ordered patch names from series file
std::vector<std::string> applied; // applied patch names from .pc/applied-patches
std::map<std::string, int> patch_strip_level; // per-patch strip level from series
std::set<std::string> patch_reversed; // patches marked -R in series
std::map<std::string, std::string> config; // merged quiltrc + env settings
// Computed helpers
ptrdiff_t top_index() const; // index of topmost applied in series (-1 if none)
bool is_applied(std::string_view patch) const;
std::optional<ptrdiff_t> find_in_series(std::string_view patch) const;
int get_strip_level(std::string_view patch) const; // returns 1 if not set
std::string get_p_format(std::string_view patch) const; // "0" or "1"
};
// I/O helpers
void out(std::string_view s);
void out_line(std::string_view s);
void err(std::string_view s);
void err_line(std::string_view s);
// Path utilities
std::string path_join(std::string_view a, std::string_view b);
std::string path_join(std::string_view a, std::string_view b, std::string_view c);
std::string basename(std::string_view path);
std::string dirname(std::string_view path);
std::string strip_trailing_slash(std::string_view s);
// String utilities
std::string trim(std::string_view s);
std::vector<std::string> split_lines(std::string_view s);
std::vector<std::string> split_on_whitespace(std::string_view s);
std::vector<std::string> shell_split(std::string_view s);
// Built-in patch engine
struct PatchOptions {
int strip_level = 1; // -pN
int fuzz = 2; // --fuzz=N (default 2)
bool reverse = false; // -R
bool dry_run = false; // --dry-run
bool force = false; // -f
bool remove_empty = false; // -E
bool quiet = false; // -s
bool merge = false; // --merge
std::string merge_style; // "" or "diff3"
// In-memory filesystem for fuzz testing. When non-null, all file I/O
// in builtin_patch uses this map instead of real syscalls.
// Key present = file exists, value = content.
std::map<std::string, std::string> *fs = nullptr;
};
struct PatchResult {
int exit_code; // 0=success, 1=rejects
std::string out; // stdout-equivalent messages
std::string err; // stderr-equivalent messages
};
PatchResult builtin_patch(std::string_view patch_text, const PatchOptions &opts);
// Built-in diff engine
enum class DiffFormat { unified, context };
enum class DiffAlgorithm { myers, minimal, patience, histogram };
std::optional<DiffAlgorithm> parse_diff_algorithm(std::string_view name);
struct DiffResult {
int exit_code; // 0 = identical, 1 = different
std::string output; // formatted diff text
};
DiffResult builtin_diff(std::string_view old_path, std::string_view new_path,
int context_lines = 3,
std::string_view old_label = {},
std::string_view new_label = {},
DiffFormat format = DiffFormat::unified,
DiffAlgorithm algorithm = DiffAlgorithm::myers,
std::map<std::string, std::string> *fs = nullptr);
// Patch name helpers — shared across command files
inline std::string_view strip_patches_prefix(const QuiltState &q, std::string_view name) {
if (name.starts_with(q.patches_dir) &&
std::ssize(name) > std::ssize(q.patches_dir) &&
name[checked_cast<size_t>(std::ssize(q.patches_dir))] == '/') {
return name.substr(checked_cast<size_t>(std::ssize(q.patches_dir) + 1));
}
return name;
}
std::string format_patch(const QuiltState &q, std::string_view name);
inline std::string patch_path_display(const QuiltState &q, std::string_view name) {
return format_patch(q, name);
}
// Resolve a user-provided file path relative to the current subdirectory.
inline std::string subdir_path(const QuiltState &q, std::string_view file) {
if (q.subdir.empty()) return std::string(file);
return q.subdir + "/" + std::string(file);
}
// Core helpers — defined in core.cpp
bool ensure_pc_dir(QuiltState &q);
std::string pc_patch_dir(const QuiltState &q, std::string_view patch);
std::vector<std::string> files_in_patch(const QuiltState &q, std::string_view patch);
bool backup_file(QuiltState &q, std::string_view patch, std::string_view file);
bool restore_file(QuiltState &q, std::string_view patch, std::string_view file);
std::vector<std::string> read_series(std::string_view path,
std::map<std::string, int> *strip_levels,
std::set<std::string> *reversed);
bool write_series(std::string_view path, std::span<const std::string> patches,
const std::map<std::string, int> &strip_levels,
const std::set<std::string> &reversed);
std::vector<std::string> read_applied(std::string_view path);
bool write_applied(std::string_view path, std::span<const std::string> patches);
// Command function type
using CmdFn = int (*)(QuiltState &q, int argc, char **argv);
struct Command {
const char *name;
CmdFn fn;
const char *usage;
const char *description;
};
// Command implementations — cmd_stack.cpp
int cmd_series(QuiltState &q, int argc, char **argv);
int cmd_applied(QuiltState &q, int argc, char **argv);
int cmd_unapplied(QuiltState &q, int argc, char **argv);
int cmd_top(QuiltState &q, int argc, char **argv);
int cmd_next(QuiltState &q, int argc, char **argv);
int cmd_previous(QuiltState &q, int argc, char **argv);
int cmd_push(QuiltState &q, int argc, char **argv);
int cmd_pop(QuiltState &q, int argc, char **argv);
// Command implementations — cmd_patch.cpp
int cmd_new(QuiltState &q, int argc, char **argv);
int cmd_add(QuiltState &q, int argc, char **argv);
int cmd_remove(QuiltState &q, int argc, char **argv);
int cmd_edit(QuiltState &q, int argc, char **argv);
int cmd_refresh(QuiltState &q, int argc, char **argv);
int cmd_diff(QuiltState &q, int argc, char **argv);
int cmd_revert(QuiltState &q, int argc, char **argv);
// Command implementations — cmd_manage.cpp
int cmd_delete(QuiltState &q, int argc, char **argv);
int cmd_rename(QuiltState &q, int argc, char **argv);
int cmd_import(QuiltState &q, int argc, char **argv);
int cmd_header(QuiltState &q, int argc, char **argv);
int cmd_files(QuiltState &q, int argc, char **argv);
int cmd_patches(QuiltState &q, int argc, char **argv);
int cmd_fold(QuiltState &q, int argc, char **argv);
int cmd_fork(QuiltState &q, int argc, char **argv);
// Command implementations — cmd_mail.cpp
int cmd_mail(QuiltState &q, int argc, char **argv);
// Command implementations — cmd_patch.cpp (continued)
int cmd_snapshot(QuiltState &q, int argc, char **argv);
int cmd_init(QuiltState &q, int argc, char **argv);
// Command implementations — cmd_manage.cpp (continued)
int cmd_upgrade(QuiltState &q, int argc, char **argv);
// Command implementations — cmd_annotate.cpp
int cmd_annotate(QuiltState &q, int argc, char **argv);
// Command implementations — cmd_graph.cpp
int cmd_graph(QuiltState &q, int argc, char **argv);
// Command stubs — cmd_stubs.cpp
int cmd_grep(QuiltState &q, int argc, char **argv);
int cmd_setup(QuiltState &q, int argc, char **argv);
int cmd_shell(QuiltState &q, int argc, char **argv);
// === src/platform.hpp ===
// This is free and unencumbered software released into the public domain.
#include <string>
#include <string_view>
#include <vector>
#include <cstdint>
// Process execution
struct ProcessResult {
int exit_code;
std::string out;
std::string err;
};
ProcessResult run_cmd(const std::vector<std::string> &argv);
ProcessResult run_cmd_input(const std::vector<std::string> &argv,
std::string_view stdin_data);
int run_cmd_tty(const std::vector<std::string> &argv);
// File system operations
std::string read_file(std::string_view path);
bool write_file(std::string_view path, std::string_view content);
bool append_file(std::string_view path, std::string_view content);
bool copy_file(std::string_view src, std::string_view dst);
bool rename_path(std::string_view old_path, std::string_view new_path);
bool delete_file(std::string_view path);
bool delete_dir_recursive(std::string_view path);
bool make_dir(std::string_view path);
bool make_dirs(std::string_view path);
bool file_exists(std::string_view path);
bool is_directory(std::string_view path);
int64_t file_mtime(std::string_view path); // -1 on failure
struct DirEntry {
std::string name;
bool is_dir;
};
std::vector<DirEntry> list_dir(std::string_view path);
// Find all regular files recursively under a directory (relative paths)
std::vector<std::string> find_files_recursive(std::string_view dir);
// Create a new unique temporary directory; returns its path, or empty on failure
std::string make_temp_dir();
// Environment
std::string get_env(std::string_view name);
void set_env(std::string_view name, std::string_view value);
std::string get_home_dir();
std::string get_cwd();
bool set_cwd(std::string_view path);
std::string get_system_quiltrc();
// I/O
void fd_write_stdout(std::string_view s);
void fd_write_stderr(std::string_view s);
bool stdout_is_tty();
// Read all of stdin
std::string read_stdin();
// Time
struct DateTime {
int year; // e.g. 2026
int month; // 1-12
int day; // 1-31
int hour; // 0-23
int min; // 0-59
int sec; // 0-59
int weekday; // 0=Sun, 1=Mon, ..., 6=Sat
int utc_offset; // seconds east of UTC
};
int64_t current_time(); // seconds since Unix epoch
DateTime local_time(int64_t timestamp); // broken-down local time + UTC offset
// Entry point called by platform main
int quilt_main(int argc, char **argv);
// === src/core.cpp ===
// This is free and unencumbered software released into the public domain.
ptrdiff_t QuiltState::top_index() const {
if (applied.empty()) return -1;
const std::string &top = applied.back();
for (ptrdiff_t i = 0; i < std::ssize(series); ++i) {
if (series[checked_cast<size_t>(i)] == top) return i;
}
return -1;
}
bool QuiltState::is_applied(std::string_view patch) const {
for (const auto &a : applied) {
if (a == patch) return true;
}
return false;
}
std::optional<ptrdiff_t> QuiltState::find_in_series(std::string_view patch) const {
for (ptrdiff_t i = 0; i < std::ssize(series); ++i) {
if (series[checked_cast<size_t>(i)] == patch) return i;
}
return std::nullopt;
}
int QuiltState::get_strip_level(std::string_view patch) const {
auto it = patch_strip_level.find(std::string(patch));
if (it != patch_strip_level.end()) return it->second;
return 1;
}
std::string QuiltState::get_p_format(std::string_view patch) const {
if (get_strip_level(patch) == 0) return "0";
return "1";
}
void out(std::string_view s) {
fd_write_stdout(s);
}
void out_line(std::string_view s) {
fd_write_stdout(s);
fd_write_stdout("\n");
}
void err(std::string_view s) {
fd_write_stderr(s);
}
void err_line(std::string_view s) {
fd_write_stderr(s);
fd_write_stderr("\n");
}
static bool is_absolute_path(std::string_view p) {
if (!p.empty() && p[0] == '/') return true;
// Windows: drive letter (e.g. C:\, D:/)
if (p.size() >= 3 && ((p[0] >= 'A' && p[0] <= 'Z') ||
(p[0] >= 'a' && p[0] <= 'z')) &&
p[1] == ':' && (p[2] == '/' || p[2] == '\\')) return true;
return false;
}
std::string path_join(std::string_view a, std::string_view b) {
if (a.empty()) return std::string(b);
if (b.empty()) return std::string(a);
if (is_absolute_path(b)) return std::string(b);
if (a.back() == '/' || a.back() == '\\') return std::string(a) + std::string(b);
return std::string(a) + "/" + std::string(b);
}
std::string path_join(std::string_view a, std::string_view b, std::string_view c) {
return path_join(path_join(a, b), c);
}
std::string basename(std::string_view path) {
if (path.empty()) return "";
// Strip trailing slashes
while (std::ssize(path) > 1 && path.back() == '/')
path.remove_suffix(1);
auto pos = str_rfind(path, '/');
if (pos < 0) return std::string(path);
return std::string(path.substr(checked_cast<size_t>(pos + 1)));
}
std::string dirname(std::string_view path) {
if (path.empty()) return ".";
// Strip trailing slashes
while (std::ssize(path) > 1 && path.back() == '/')
path.remove_suffix(1);
auto pos = str_rfind(path, '/');
if (pos < 0) return ".";
if (pos == 0) return "/";
return std::string(path.substr(0, checked_cast<size_t>(pos)));
}
std::string strip_trailing_slash(std::string_view s) {
while (!s.empty() && s.back() == '/')
s.remove_suffix(1);
return s.empty() ? std::string("/") : std::string(s);
}
std::string format_patch(const QuiltState &q, std::string_view name) {
if (!get_env("QUILT_PATCHES_PREFIX").empty()) {
return q.patches_dir + "/" + std::string(name);
}
return std::string(name);
}
std::string trim(std::string_view s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t' ||
s.front() == '\r' || s.front() == '\n'))
s.remove_prefix(1);
while (!s.empty() && (s.back() == ' ' || s.back() == '\t' ||
s.back() == '\r' || s.back() == '\n'))
s.remove_suffix(1);
return std::string(s);
}
std::vector<std::string> split_lines(std::string_view s) {
std::vector<std::string> lines;
while (!s.empty()) {
auto pos = str_find(s, '\n');
if (pos < 0) {
if (!s.empty() && s.back() == '\r')
s.remove_suffix(1);
lines.emplace_back(s);
break;
}
auto end = checked_cast<size_t>(pos);
if (end > 0 && s[end - 1] == '\r')
--end;
lines.emplace_back(s.substr(0, end));
s.remove_prefix(checked_cast<size_t>(pos + 1));
}
return lines;
}
std::vector<std::string> split_on_whitespace(std::string_view s) {
std::vector<std::string> tokens;
ptrdiff_t i = 0;
while (i < std::ssize(s)) {
while (i < std::ssize(s) && (s[checked_cast<size_t>(i)] == ' ' || s[checked_cast<size_t>(i)] == '\t'))
++i;
if (i >= std::ssize(s)) break;
ptrdiff_t start = i;
while (i < std::ssize(s) && s[checked_cast<size_t>(i)] != ' ' && s[checked_cast<size_t>(i)] != '\t')
++i;
tokens.emplace_back(s.substr(checked_cast<size_t>(start), checked_cast<size_t>(i - start)));
}
return tokens;
}
static bool is_varname_start(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
}
static bool is_varname_char(char c) {
return is_varname_start(c) || (c >= '0' && c <= '9');
}
static std::string expand_var(std::string_view s, ptrdiff_t &i) {
// i points just past '$'
std::string name;
if (i < std::ssize(s) && s[checked_cast<size_t>(i)] == '{') {
++i; // skip '{'
while (i < std::ssize(s) && s[checked_cast<size_t>(i)] != '}') {
name += s[checked_cast<size_t>(i++)];
}
if (i < std::ssize(s)) ++i; // skip '}'
} else {
while (i < std::ssize(s) && is_varname_char(s[checked_cast<size_t>(i)])) {
name += s[checked_cast<size_t>(i++)];
}
}
if (name.empty()) return "$";
return get_env(name);
}
std::vector<std::string> shell_split(std::string_view s) {
std::vector<std::string> tokens;
ptrdiff_t i = 0;
while (i < std::ssize(s)) {
// Skip whitespace between tokens
while (i < std::ssize(s) && (s[checked_cast<size_t>(i)] == ' ' || s[checked_cast<size_t>(i)] == '\t'))
++i;
if (i >= std::ssize(s)) break;
std::string tok;
// Accumulate segments until unquoted whitespace
while (i < std::ssize(s) && s[checked_cast<size_t>(i)] != ' ' && s[checked_cast<size_t>(i)] != '\t') {
if (s[checked_cast<size_t>(i)] == '\'') {
// Single-quoted: literal, no escapes, no variable expansion
++i;
while (i < std::ssize(s) && s[checked_cast<size_t>(i)] != '\'')
tok += s[checked_cast<size_t>(i++)];
if (i < std::ssize(s)) ++i; // skip closing '
} else if (s[checked_cast<size_t>(i)] == '"') {
// Double-quoted: backslash escapes and variable expansion
++i;
while (i < std::ssize(s) && s[checked_cast<size_t>(i)] != '"') {
if (s[checked_cast<size_t>(i)] == '\\' && i + 1 < std::ssize(s)) {
char next = s[checked_cast<size_t>(i + 1)];
if (next == '"' || next == '\\' || next == '$') {
tok += next;
i += 2;
continue;
}
}
if (s[checked_cast<size_t>(i)] == '$') {
++i;
tok += expand_var(s, i);
continue;
}
tok += s[checked_cast<size_t>(i++)];
}
if (i < std::ssize(s)) ++i; // skip closing "
} else if (s[checked_cast<size_t>(i)] == '$') {
// Unquoted variable expansion
++i;
tok += expand_var(s, i);
} else if (s[checked_cast<size_t>(i)] == '\\' && i + 1 < std::ssize(s)) {
// Unquoted backslash escape
tok += s[checked_cast<size_t>(i + 1)];
i += 2;
} else {
tok += s[checked_cast<size_t>(i++)];
}
}
if (!tok.empty()) {
tokens.push_back(std::move(tok));
}
}
return tokens;
}
std::vector<std::string> read_series(std::string_view path,
std::map<std::string, int> *strip_levels,
std::set<std::string> *reversed) {
std::vector<std::string> patches;
std::string content = read_file(path);
if (content.empty()) return patches;
auto lines = split_lines(content);
for (auto &line : lines) {
std::string trimmed = trim(line);
if (trimmed.empty()) continue;
if (trimmed[0] == '#') continue;
// Strip inline comments
auto hash = str_find(std::string_view(trimmed), " #");
if (hash >= 0) {
trimmed = trim(std::string_view(trimmed).substr(0, checked_cast<size_t>(hash)));
}
// Split into tokens
auto tokens = split_on_whitespace(trimmed);
if (tokens.empty()) continue;
std::string name = tokens[0];
// Parse options (e.g., "-p0", "-p 0", "-R")
int strip = 1;
bool is_reversed = false;
for (ptrdiff_t i = 1; i < std::ssize(tokens); ++i) {
if (tokens[checked_cast<size_t>(i)] == "-p" && i + 1 < std::ssize(tokens)) {
strip = checked_cast<int>(parse_int(tokens[checked_cast<size_t>(i + 1)]));
++i;
} else if (tokens[checked_cast<size_t>(i)].starts_with("-p") && std::ssize(tokens[checked_cast<size_t>(i)]) > 2) {
strip = checked_cast<int>(parse_int(tokens[checked_cast<size_t>(i)].substr(2)));
} else if (tokens[checked_cast<size_t>(i)] == "-R") {
is_reversed = true;
}
}
if (strip_levels && strip != 1) {
(*strip_levels)[name] = strip;
}
if (reversed && is_reversed) {
reversed->insert(name);
}
patches.push_back(std::move(name));
}
return patches;
}
bool write_series(std::string_view path, std::span<const std::string> patches,
const std::map<std::string, int> &strip_levels,
const std::set<std::string> &reversed) {
std::string content;
for (const auto &p : patches) {
content += p;
auto it = strip_levels.find(p);
if (it != strip_levels.end() && it->second != 1) {
content += " -p";
content += std::to_string(it->second);
}
if (reversed.contains(p)) {
content += " -R";
}
content += '\n';
}
return write_file(path, content);
}
std::vector<std::string> read_applied(std::string_view path) {
std::vector<std::string> patches;
std::string content = read_file(path);
if (content.empty()) return patches;
auto lines = split_lines(content);
for (auto &line : lines) {
std::string trimmed = trim(line);
if (!trimmed.empty()) {
patches.push_back(std::move(trimmed));
}
}
return patches;
}
bool write_applied(std::string_view path, std::span<const std::string> patches) {
std::string content;
for (const auto &p : patches) {
content += p;
content += '\n';
}
return write_file(path, content);
}
bool ensure_pc_dir(QuiltState &q) {
std::string pc = path_join(q.work_dir, q.pc_dir);
if (!is_directory(pc)) {
if (!make_dirs(pc)) {
err_line("Failed to create " + pc);
return false;
}
}
// Write .version
std::string version_path = path_join(pc, ".version");
if (!file_exists(version_path)) {
if (!write_file(version_path, "2\n")) {
err_line("Failed to write " + version_path);
return false;
}
}
// Write .quilt_patches
std::string qp_path = path_join(pc, ".quilt_patches");
if (!file_exists(qp_path)) {
if (!write_file(qp_path, q.patches_dir + "\n")) {
err_line("Failed to write " + qp_path);
return false;
}
}
// Write .quilt_series
std::string qs_path = path_join(pc, ".quilt_series");
if (!file_exists(qs_path)) {
std::string series_name = get_env("QUILT_SERIES");
if (series_name.empty()) series_name = "series";
if (!write_file(qs_path, series_name + "\n")) {
err_line("Failed to write " + qs_path);
return false;
}
}
return true;
}
// Parse a simplified subset of bash KEY=VALUE assignments from a quiltrc file.
// Supports: KEY=value, KEY="value", KEY='value', export KEY=value
// Skips comments (#), blank lines, and lines that aren't assignments.
static std::map<std::string, std::string> parse_quiltrc(std::string_view content) {
std::map<std::string, std::string> result;
auto lines = split_lines(content);
for (auto &line : lines) {
std::string_view sv = line;
// Strip leading whitespace
while (!sv.empty() && (sv.front() == ' ' || sv.front() == '\t'))
sv.remove_prefix(1);
// Skip blank lines and comments
if (sv.empty() || sv.front() == '#') continue;
// Strip optional "export " prefix
if (std::ssize(sv) > 7 && sv.substr(0, 7) == "export ") {
sv.remove_prefix(7);
while (!sv.empty() && (sv.front() == ' ' || sv.front() == '\t'))
sv.remove_prefix(1);
}
// Find '=' for KEY=VALUE
auto eq = str_find(sv, '=');
if (eq <= 0) continue;
// Validate key: must be alphanumeric/underscore
std::string_view key = sv.substr(0, checked_cast<size_t>(eq));
bool valid_key = true;
for (char c : key) {
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '_')) {
valid_key = false;
break;
}
}
if (!valid_key) continue;
// Parse value
std::string_view rest = sv.substr(checked_cast<size_t>(eq + 1));
std::string value;
if (!rest.empty() && rest.front() == '"') {
// Double-quoted: handle \" and \\ escapes
rest.remove_prefix(1);
while (!rest.empty() && rest.front() != '"') {
if (rest.front() == '\\' && std::ssize(rest) > 1) {
char next = rest[1];
if (next == '"' || next == '\\') {
value += next;
rest.remove_prefix(2);
continue;
}
}
value += rest.front();
rest.remove_prefix(1);
}
} else if (!rest.empty() && rest.front() == '\'') {
// Single-quoted: literal, no escapes
rest.remove_prefix(1);
while (!rest.empty() && rest.front() != '\'') {
value += rest.front();
rest.remove_prefix(1);
}
} else {
// Unquoted: ends at whitespace, line ending, or #
while (!rest.empty() && rest.front() != ' ' && rest.front() != '\t' &&
rest.front() != '\r' && rest.front() != '\n' &&
rest.front() != '#') {
value += rest.front();
rest.remove_prefix(1);
}
}
result[std::string(key)] = value;
}
return result;
}
// Load and parse the quiltrc file. If quiltrc_path is empty, use default
// search order (~/.quiltrc, /etc/quilt.quiltrc). If "-", skip loading.
static std::map<std::string, std::string> load_quiltrc(std::string_view quiltrc_path) {
if (quiltrc_path == "-") return {};
if (!quiltrc_path.empty()) {
std::string content = read_file(quiltrc_path);
if (!content.empty()) return parse_quiltrc(content);
return {};
}
// Default search order
std::string home = get_home_dir();
if (!home.empty()) {
std::string user_rc = path_join(home, ".quiltrc");
std::string content = read_file(user_rc);
if (!content.empty()) return parse_quiltrc(content);
}
std::string sys_rc = get_system_quiltrc();
std::string content = read_file(sys_rc);
if (!content.empty()) return parse_quiltrc(content);
return {};
}
QuiltState load_state() {
QuiltState q;
q.patches_dir = "patches";
q.pc_dir = ".pc";
// Read environment variable overrides
std::string env_pc = get_env("QUILT_PC");
if (!env_pc.empty()) q.pc_dir = env_pc;
std::string env_patches = get_env("QUILT_PATCHES");
if (!env_patches.empty()) q.patches_dir = env_patches;
// Read QUILT_SERIES override for series filename
std::string env_series = get_env("QUILT_SERIES");
// Upward directory scan: find project root containing .pc/ or patches/
// Only use relative paths for scanning; absolute paths are used as-is.
std::string cwd = get_cwd();
std::string scan = cwd;
bool patches_dir_is_abs = is_absolute_path(q.patches_dir);
while (true) {
if (is_directory(path_join(scan, q.pc_dir)) ||
(!patches_dir_is_abs && is_directory(path_join(scan, q.patches_dir)))) {
break;
}
std::string parent = dirname(scan);
if (parent == scan) {
// Reached filesystem root without finding anything; use cwd
scan = cwd;
break;
}
scan = parent;
}
q.work_dir = scan;
if (q.work_dir != cwd) {
// Compute subdirectory: strip work_dir prefix + '/' from cwd
q.subdir = cwd.substr(q.work_dir.size() + 1);
set_cwd(q.work_dir);
}
// Check if .pc/ exists and read overrides
std::string pc_abs = path_join(q.work_dir, q.pc_dir);
std::string series_name_override;
if (is_directory(pc_abs)) {
// Read .quilt_patches override
std::string qp = trim(read_file(path_join(pc_abs, ".quilt_patches")));
if (!qp.empty()) {
q.patches_dir = qp;
}
// Read .quilt_series override
std::string qs = trim(read_file(path_join(pc_abs, ".quilt_series")));
if (!qs.empty()) {
series_name_override = qs;
}
}
// Series file search order (when not overridden by .quilt_series)
if (q.series_file.empty()) {
std::string series_name = !series_name_override.empty()
? series_name_override
: (env_series.empty() ? "series" : env_series);
std::string s1 = path_join(q.work_dir, series_name);
std::string s2 = path_join(q.work_dir, q.patches_dir, series_name);
std::string s3 = path_join(q.work_dir, q.pc_dir, series_name);
if (file_exists(s3)) {
q.series_file = path_join(q.pc_dir, series_name);
} else if (file_exists(s1)) {
q.series_file = series_name;
} else if (file_exists(s2)) {
q.series_file = path_join(q.patches_dir, series_name);
} else {
q.series_file = path_join(q.patches_dir, series_name);
}
}
// Read series file
std::string series_abs = path_join(q.work_dir, q.series_file);
q.series_file_exists = file_exists(series_abs);
q.series = read_series(series_abs, &q.patch_strip_level, &q.patch_reversed);
// Read applied-patches file
std::string applied_abs = path_join(q.work_dir, q.pc_dir, "applied-patches");
q.applied = read_applied(applied_abs);
return q;
}
std::string pc_patch_dir(const QuiltState &q, std::string_view patch) {
return path_join(q.work_dir, q.pc_dir, patch);
}
std::vector<std::string> files_in_patch(const QuiltState &q, std::string_view patch) {
std::string dir = pc_patch_dir(q, patch);
if (!is_directory(dir)) return {};
auto all = find_files_recursive(dir);
std::vector<std::string> result;
for (auto &f : all) {
// Skip quilt metadata files (e.g. .timestamp, .needs_refresh)
auto slash = str_rfind(std::string_view(f), '/');
std::string_view base = (slash >= 0)
? std::string_view(f).substr(checked_cast<size_t>(slash + 1))
: std::string_view(f);
if (!base.empty() && base[0] == '.') continue;
result.push_back(std::move(f));
}
return result;
}
bool backup_file(QuiltState &q, std::string_view patch, std::string_view file) {
std::string src = path_join(q.work_dir, file);
std::string dst = path_join(pc_patch_dir(q, patch), file);
// Ensure destination directory exists
std::string dst_dir = dirname(dst);
if (!is_directory(dst_dir)) {
if (!make_dirs(dst_dir)) {
err_line("Failed to create directory: " + dst_dir);
return false;
}
}
if (file_exists(src)) {
return copy_file(src, dst);
} else {
// File doesn't exist yet; create an empty placeholder
return write_file(dst, "");
}
}
bool restore_file(QuiltState &q, std::string_view patch, std::string_view file) {
std::string backup = path_join(pc_patch_dir(q, patch), file);
std::string target = path_join(q.work_dir, file);
if (!file_exists(backup)) {
err("No backup for "); err_line(file);
return false;
}
std::string content = read_file(backup);
if (content.empty()) {
// Zero-length backup = file didn't exist before the patch — remove target
if (file_exists(target) && !delete_file(target)) {
err_line("Failed to remove " + target);
return false;
}
return true;
}
// Ensure target directory exists
std::string target_dir = dirname(target);
if (!is_directory(target_dir)) {
if (!make_dirs(target_dir)) {
err_line("Failed to create directory: " + target_dir);
return false;
}
}
return write_file(target, content);
}
std::string to_cstr(std::string_view s) {
return std::string(s);
}
static Command commands[] = {
{"new", cmd_new,
"Usage: quilt new [-p n] patchname\n"
"\n"
"Create a new empty patch and insert it after the topmost applied\n"
"patch in the series. The new patch becomes the top of the stack\n"
"immediately, but no patch file is written until quilt refresh.\n"
"\n"
"Options:\n"
" -p n Set the strip level for the patch (default: 1).\n",
"Create a new empty patch"},
{"add", cmd_add,
"Usage: quilt add [-P patch] file ...\n"
"\n"
"Register files with the topmost patch by backing up their current\n"
"contents. Files must be added before modification so that quilt\n"
"can capture the pre-change state. Use quilt edit to add and open\n"
"files in a single step.\n"
"\n"
"Options:\n"
" -P patch Add files to the named patch instead of the top.\n",
"Add files to the topmost patch"},
{"push", cmd_push,
"Usage: quilt push [-afqv] [--fuzz=N] [-m] [--merge[=merge|diff3]]\n"
" [--leave-rejects] [--refresh] [num|patch]\n"
"\n"
"Apply the next unapplied patch from the series. Without arguments,\n"
"applies one patch. With a patch name, applies patches up to and\n"
"including it. With a number, applies that many patches.\n"
"\n"
"Options:\n"
" -a Apply all unapplied patches.\n"
" -f Force apply even when the patch has rejects.\n"
" -q Quiet; print only error messages.\n"
" -v Verbose; pass --verbose to patch.\n"
" --fuzz=N Set the maximum fuzz factor for patch.\n"
" -m, --merge[=merge|diff3]\n"
" Merge using patch's merge mode.\n"
" --leave-rejects Leave .rej files in the working tree.\n"
" --refresh Refresh each patch after applying.\n",
"Apply patches to the source tree"},
{"pop", cmd_pop,
"Usage: quilt pop [-afRqv] [--refresh] [num|patch]\n"
"\n"
"Remove the topmost applied patch by restoring files from backup.\n"
"Without arguments, removes one patch. With a patch name, removes\n"
"patches until the named patch is on top. With a number, removes\n"
"that many patches.\n"
"\n"
"Options:\n"
" -a Remove all applied patches.\n"
" -f Force removal even if the patch needs refresh.\n"
" -R Always verify that the patch removes cleanly.\n"
" -q Quiet; print only error messages.\n"
" -v Verbose; print file-level restore messages.\n"
" --refresh Automatically refresh every patch before it gets unapplied.\n",
"Remove applied patches from the stack"},
{"refresh", cmd_refresh,
"Usage: quilt refresh [-p n] [-u | -U num | -c | -C num] [-z [new_name]]\n"
" [-f] [--no-timestamps] [--no-index] [--diffstat] [--sort]\n"
" [--strip-trailing-whitespace] [--backup]\n"
" [--diff-algorithm={myers|minimal|patience|histogram}] [patch]\n"
"\n"
"Regenerate the topmost or named patch by diffing backup copies in\n"
".pc/ against the current working tree. This is what actually writes\n"
"the patch file; changes are not recorded until you refresh.\n"
"\n"
"Options:\n"
" -p n Set the path label style (0, 1, or ab).\n"
" -u Create a unified diff (default).\n"
" -U num Create a unified diff with num lines of context.\n"
" -c Create a context diff.\n"
" -C num Create a context diff with num lines of context.\n"
" -z [new_name] Create a new patch (fork) containing the changes;\n"
" the current patch is left as-is.\n"
" -f Refresh even when files are shadowed by patches\n"
" applied above.\n"
" --no-timestamps Omit timestamps from diff headers.\n"
" --no-index Omit Index: lines from the patch.\n"
" --diffstat Add a diffstat section to the patch header.\n"
" --sort Sort files alphabetically in the patch.\n"
" --strip-trailing-whitespace\n"
" Strip trailing whitespace from each line.\n"
" --backup Save the old patch file as name~ before updating.\n"
" --diff-algorithm=name\n"
" Select the diff algorithm: myers (default),\n"
" minimal, patience, or histogram.\n"
"\n"
"The QUILT_DIFF_ALGORITHM environment variable sets the default\n"
"algorithm (overridden by --diff-algorithm on the command line).\n",
"Regenerate a patch from working tree changes"},
{"diff", cmd_diff,
"Usage: quilt diff [-p n] [-u | -U num | -c | -C num]\n"
" [--combine patch] [-P patch] [-z] [-R] [--snapshot]\n"
" [--diff=utility] [--no-timestamps] [--no-index] [--sort]\n"
" [--diff-algorithm={myers|minimal|patience|histogram}] [file ...]\n"
"\n"
"Show the diff that quilt refresh would produce for the topmost or\n"
"named patch. Without -z, shows the full patch content (backup vs.\n"
"working tree). With -z, shows only uncommitted changes since the\n"
"last refresh.\n"
"\n"
"Options:\n"
" -p n Set the path label style (0, 1, or ab).\n"
" -u Create a unified diff (default).\n"
" -U num Create a unified diff with num lines of context.\n"
" -c Create a context diff.\n"
" -C num Create a context diff with num lines of context.\n"
" --combine patch Create a combined diff for all patches between\n"
" this patch and the topmost or specified patch.\n"
" A patch name of '-' is the first applied patch.\n"
" -P patch Show the diff for the named patch.\n"
" -z Show only changes since the last refresh.\n"
" -R Produce a reverse diff.\n"
" --snapshot Diff against a previously saved snapshot.\n"
" --diff=utility Use the specified diff utility instead of the\n"
" built-in diff engine.\n"
" --no-timestamps Omit timestamps from diff headers.\n"
" --no-index Omit Index: lines from the output.\n"
" --sort Sort files alphabetically in the output.\n"
" --diff-algorithm=name\n"
" Select the diff algorithm: myers (default),\n"
" minimal, patience, or histogram.\n"
"\n"
"The QUILT_DIFF_ALGORITHM environment variable sets the default\n"
"algorithm (overridden by --diff-algorithm on the command line).\n",
"Show the diff of the topmost or a specified patch"},
{"series", cmd_series,
"Usage: quilt series [-v]\n"
"\n"
"List all patches in the series file, both applied and unapplied.\n"
"\n"
"Options:\n"
" -v Mark applied patches with = and the top with =.\n",
"List all patches in the series"},
{"applied", cmd_applied,
"Usage: quilt applied [patch]\n"
"\n"
"List the currently applied patches in stack order. With a patch\n"
"name, lists all applied patches up to and including it.\n",
"List applied patches"},
{"unapplied", cmd_unapplied,
"Usage: quilt unapplied [patch]\n"
"\n"
"List the patches that have not been applied yet. With a patch\n"
"name, lists all patches after the named one in the series.\n",
"List patches not yet applied"},
{"top", cmd_top,
"Usage: quilt top\n"
"\n"
"Print the name of the topmost applied patch.\n",
"Show the topmost applied patch"},
{"next", cmd_next,
"Usage: quilt next [patch]\n"
"\n"
"Print the patch after the topmost applied patch, or after the\n"
"named patch in the series.\n",
"Show the next patch after the top or a given patch"},
{"previous", cmd_previous,
"Usage: quilt previous [patch]\n"
"\n"
"Print the patch before the topmost applied patch, or before the\n"
"named patch in the series.\n",
"Show the patch before the top or a given patch"},
{"delete", cmd_delete,
"Usage: quilt delete [-r] [--backup] [-n] [patch]\n"
"\n"
"Remove the topmost applied patch or a named unapplied patch from\n"
"the series. The patch file is kept unless -r is given.\n"
"\n"
"Options:\n"
" -r Remove the patch file as well.\n"
" --backup Rename the patch file to name~ instead of deleting.\n"
" -n Delete the next unapplied patch instead of the top.\n",
"Remove a patch from the series"},
{"rename", cmd_rename,
"Usage: quilt rename [-P patch] new_name\n"
"\n"
"Rename the topmost or named patch. Updates the series file and\n"
"renames the patch file in the patches directory.\n"
"\n"
"Options:\n"
" -P patch Rename the named patch instead of the top.\n",
"Rename a patch"},
{"import", cmd_import,
"Usage: quilt import [-p n] [-R] [-P name] [-f] [-d {o|a|n}] file ...\n"
"\n"
"Copy an external patch file into the patches directory and add it\n"
"to the series after the topmost applied patch. The patch is not\n"
"applied; use quilt push afterward.\n"
"\n"
"Options:\n"
" -p n Set the strip level for the imported patch.\n"
" -R Apply patch in reverse.\n"
" -P name Use this name instead of the original filename.\n"
" -f Overwrite if a patch with the same name exists.\n"
" -d {o|a|n} When overwriting: keep old, append all, or use\n"
" new header.\n",
"Import an external patch into the series"},
{"header", cmd_header,
"Usage: quilt header [-a|-r|-e] [--backup] [--dep3]\n"
" [--strip-diffstat] [--strip-trailing-whitespace] [patch]\n"
"\n"
"Print the header (description) of the topmost or named patch.\n"
"The header is all text in the patch file before the first diff.\n"
"\n"
"Options:\n"
" -a Append text from standard input to the header.\n"
" -r Replace the header with text from standard input.\n"
" -e Open the header in $EDITOR.\n"
" --backup Save the old patch file as name~ before modifying.\n"
" --dep3 Insert DEP-3 template when editing empty headers.\n"
" --strip-diffstat Remove the diffstat section from the header.\n"
" --strip-trailing-whitespace\n"
" Strip trailing whitespace from each header line.\n",
"Print or modify a patch header"},
{"files", cmd_files,
"Usage: quilt files [-v] [-a] [-l] [--combine patch] [patch]\n"
"\n"
"List the files that the topmost or named patch modifies.\n"
"\n"
"Options:\n"
" -v Show the patch name alongside each filename.\n"
" -a List files for all applied patches, not just one.\n"
" -l Add patch name to output lines.\n"
" --combine patch List files for a range of patches.\n",
"List files modified by a patch"},
{"patches", cmd_patches,
"Usage: quilt patches [-v] file ...\n"
"\n"
"List the patches that modify the given file or files. Searches\n"
"both applied patches (via .pc/ metadata) and unapplied patches\n"
"(by parsing patch files).\n"
"\n"
"Options:\n"
" -v Mark applied patches in the output.\n",
"List patches that modify a given file"},
{"edit", cmd_edit,
"Usage: quilt edit file ...\n"
"\n"
"Add files to the topmost patch and open them in $EDITOR. This is\n"
"a shortcut for quilt add followed by $EDITOR, and is the safest\n"
"way to modify tracked files.\n",
"Add files to the topmost patch and open an editor"},
{"revert", cmd_revert,
"Usage: quilt revert [-P patch] file ...\n"
"\n"
"Discard uncommitted changes to files by restoring them from the\n"
"backup copies in .pc/. Only reverts changes not yet captured by\n"
"quilt refresh.\n"
"\n"
"Options:\n"
" -P patch Revert files in the named patch instead of the top.\n",
"Discard working tree changes to files in a patch"},
{"remove", cmd_remove,
"Usage: quilt remove [-P patch] file ...\n"
"\n"
"Remove files from the topmost or named patch and restore them\n"
"from backup. The opposite of quilt add.\n"
"\n"
"Options:\n"
" -P patch Remove files from the named patch instead of the top.\n",
"Remove files from the topmost patch"},
{"fold", cmd_fold,
"Usage: quilt fold [-R] [-q] [-f] [-p n]\n"
"\n"
"Fold a diff read from standard input into the topmost patch.\n"
"Files touched by the incoming diff are automatically added to\n"
"the patch. Run quilt refresh afterward to update the patch file.\n"
"\n"
"Options:\n"
" -R Apply the diff in reverse.\n"
" -q Quiet; print only error messages.\n"
" -f Force apply even when the diff has rejects.\n"
" -p n Set the strip level for the incoming diff.\n",
"Fold a diff from stdin into the topmost patch"},
{"fork", cmd_fork,
"Usage: quilt fork [new_name]\n"
"\n"
"Copy the topmost patch to a new name. The series is updated to\n"
"reference the copy; the original file is kept but removed from\n"
"the series. If no name is given, -2 is appended (or -3, etc.).\n",
"Create a copy of the topmost patch under a new name"},
// Implemented analysis commands
{"annotate", cmd_annotate,
"Usage: quilt annotate [-P patch] file\n"
"\n"
"Show which applied patch last modified each line of a file,\n"
"similar to git blame. Works by comparing successive backup\n"
"copies in .pc/.\n"
"\n"
"Options:\n"
" -P patch Stop at the named patch instead of the top.\n",
"Show which patch modified each line of a file"},
{"graph", cmd_graph,
"Usage: quilt graph [--all] [--reduce] [--lines[=num]]\n"
" [--edge-labels=files] [patch]\n"
"\n"
"Print a dot-format dependency graph of applied patches. Two\n"
"patches are dependent if they modify the same file, or with\n"
"--lines, if their changes overlap.\n"
"\n"
"Options:\n"
" --all Include all applied patches (default: only\n"
" dependencies of the top or named patch).\n"
" --reduce Remove transitive edges from the graph.\n"
" --lines[=num] Compute line-level dependencies using num\n"
" lines of context (default: 2).\n"
" --edge-labels=files Label edges with shared filenames.\n",
"Print a dot dependency graph of applied patches"},
{"mail", cmd_mail,
"Usage: quilt mail {--mbox file} [--prefix prefix] [--sender addr]\n"
" [--from addr] [--to addr] [--cc addr] [--bcc addr]\n"
" [first_patch [last_patch]]\n"
"\n"
"Generate an mbox file containing one message per patch in the\n"
"given range. Output is intended for git am. Either --from or\n"
"--sender is required.\n"
"\n"
"Options:\n"
" --mbox file Write output to file (required).\n"
" --prefix prefix Subject line prefix (default: PATCH).\n"
" --sender addr Set the envelope sender address.\n"
" --from addr Set the From: header address.\n"
" --to addr Add a To: recipient (repeatable).\n"
" --cc addr Add a Cc: recipient (repeatable).\n"
" --bcc addr Add a Bcc: recipient (repeatable).\n",
"Generate an mbox file from a range of patches"},
// Stubs
{"grep", cmd_grep,
"Usage: quilt grep [-h|options] pattern\n"
"\n"
"Search source files, skipping patches/ and .pc/ directories.\n"
"Not yet implemented.\n",
"Search source files (not implemented)"},
{"setup", cmd_setup,
"Usage: quilt setup [-d path] series\n"
"\n"
"Initialize a source tree from a series file or RPM spec.\n"
"Not yet implemented.\n",
"Set up a source tree from a series file (not implemented)"},
{"shell", cmd_shell,
"Usage: quilt shell [command]\n"
"\n"
"Open a shell or run a command in the quilt environment.\n"
"Not yet implemented.\n",
"Open a subshell (not implemented)"},
{"snapshot", cmd_snapshot,
"Usage: quilt snapshot [-d]\n"
"\n"
"Save a copy of the current working tree state for later\n"
"comparison with quilt diff --snapshot.\n"
"\n"
"Options:\n"
" -d Remove the current snapshot instead of creating one.\n",
"Save a snapshot of the working tree for later diff"},
{"upgrade", cmd_upgrade,
"Usage: quilt upgrade\n"
"\n"
"Upgrade quilt metadata in .pc/ to the current format. This is\n"
"a no-op because only the version 2 format is supported.\n",
"Upgrade quilt metadata to the current format"},
{"init", cmd_init,
"Usage: quilt init\n"
"\n"
"Initialize quilt metadata in the current directory. This is\n"
"optional since any quilt command creates .pc/ and patches/ as\n"
"needed, but it lets you establish the project root before\n"
"working from a subdirectory.\n",
"Initialize quilt metadata in the current directory"},
};
static constexpr int num_commands = sizeof(commands) / sizeof(commands[0]);
static std::string to_upper(std::string_view s) {
std::string result(s);
for (char &c : result) {
if (c >= 'a' && c <= 'z') c -= 32;
}
return result;
}
int quilt_main(int argc, char **argv) {
// --- Phase 1: Extract global options (--quiltrc) from argv ---
std::string quiltrc_path; // empty = default search, "-" = disabled
bool quiltrc_set = false;
std::vector<char *> clean_argv;
clean_argv.push_back(argv[0]);
for (int i = 1; i < argc; ++i) {
std::string_view a = argv[i];
if (a == "--quiltrc" && i + 1 < argc) {
quiltrc_path = argv[i + 1];
quiltrc_set = true;
++i; // skip the argument
continue;
}
if (a.starts_with("--quiltrc=")) {
quiltrc_path = std::string(a.substr(10));
quiltrc_set = true;
continue;
}
if (a == "--trace") {
continue; // accepted but ignored
}
clean_argv.push_back(argv[i]);
}
int clean_argc = checked_cast<int>(std::ssize(clean_argv));
// Handle no arguments
if (clean_argc < 2) {
err_line("Usage: quilt [--quiltrc file] <command> [options] [args]");
err_line("Use \"quilt --help\" for a list of commands.");
return 1;
}
std::string_view arg1 = clean_argv[1];
// Handle --version
if (arg1 == "--version" || arg1 == "-v") {
out_line(QUILT_VERSION);
return 0;
}
// Handle --help
if (arg1 == "--help" || arg1 == "-h" || arg1 == "help") {
out_line("Usage: quilt [--quiltrc file] <command> [options] [args]");
out_line("");
out_line("Commands:");
ptrdiff_t max_len = 0;
for (int i = 0; i < num_commands; ++i) {
ptrdiff_t len = std::ssize(std::string_view(commands[i].name));
if (len > max_len) max_len = len;
}
for (int i = 0; i < num_commands; ++i) {
std::string line = " ";
line += commands[i].name;
line.append(checked_cast<size_t>(max_len + 2 - std::ssize(std::string_view(commands[i].name))), ' ');
line += commands[i].description;
out_line(line);
}
out_line("");
out_line("Use \"quilt <command> --help\" for details on a specific command.");
return 0;
}
// --- Phase 2: Load quiltrc and populate environment ---
auto rc_vars = load_quiltrc(quiltrc_set ? quiltrc_path : std::string());
// Apply quiltrc values to environment. GNU quilt sources quiltrc into the
// command process, so assignments there override inherited environment
// values unless the rc itself chooses otherwise.
for (auto &kv : rc_vars) {
set_env(kv.first, kv.second);
}
// --- Phase 3: Find command ---
std::string cmd_name(arg1);
// Find command (supports unique prefix abbreviation)
Command *found = nullptr;
int match_count = 0;
for (int i = 0; i < num_commands; ++i) {
if (cmd_name == commands[i].name) {
found = &commands[i];
match_count = 1;
break;
}
if (std::string_view(commands[i].name).starts_with(cmd_name)) {
found = &commands[i];
match_count++;
}
}
if (match_count > 1) {
err_line("quilt: command '" + cmd_name + "' is ambiguous");
return 1;
}
if (!found) {
err_line("quilt: unknown command '" + cmd_name + "'");
err_line("Use \"quilt --help\" for a list of commands.");
return 1;
}
// Handle per-command -h/--help before dispatching
for (int i = 2; i < clean_argc; ++i) {
std::string_view a = clean_argv[checked_cast<size_t>(i)];
if (a == "-h" || a == "--help") {
out_line(found->usage);
return 0;
}
}
// --- Phase 4: Load state ---
std::string original_cwd = get_cwd();
QuiltState q = load_state();
if (std::string_view(found->name) == "init" && get_cwd() != original_cwd) {
set_cwd(original_cwd);
}
q.config = rc_vars;
// Merge env overrides into config
for (auto &kv : rc_vars) {
std::string env_val = get_env(kv.first);
if (!env_val.empty()) {
q.config[kv.first] = env_val;
}
}
// --- Phase 5: Inject QUILT_COMMAND_ARGS ---
std::string args_key = "QUILT_" + to_upper(found->name) + "_ARGS";
std::string cmd_args = get_env(args_key);
auto extra_args = shell_split(cmd_args);
// Build the final argv for the command: [cmd_name, extra_args..., user_args...]
// Command argv starts at clean_argv+1
std::vector<std::string> final_argv_storage;
std::vector<char *> final_argv;
final_argv_storage.push_back(std::string(found->name));
for (auto &ea : extra_args) {
final_argv_storage.push_back(ea);
}
for (int i = 2; i < clean_argc; ++i) {
final_argv_storage.push_back(clean_argv[checked_cast<size_t>(i)]);
}
for (auto &s : final_argv_storage) {
final_argv.push_back(const_cast<char *>(s.c_str()));
}
// Dispatch
return found->fn(q, checked_cast<int>(std::ssize(final_argv)), final_argv.data());
}
// === src/diff.cpp ===
// This is free and unencumbered software released into the public domain.
//
// Built-in diff engine: Myers, minimal, and patience algorithms.
// Produces unified or context diff output, replacing the need for an
// external diff binary.
#include <algorithm>
#include <cstdio>
#include <optional>
#include <unordered_map>
std::optional<DiffAlgorithm> parse_diff_algorithm(std::string_view name)
{
if (name == "myers") return DiffAlgorithm::myers;
if (name == "minimal") return DiffAlgorithm::minimal;
if (name == "patience") return DiffAlgorithm::patience;
if (name == "histogram") return DiffAlgorithm::histogram;
return std::nullopt;
}
// Approximate integer square root: next power of 2 >= sqrt(n).
// Matches libxdiff's xdl_bogosqrt().
static ptrdiff_t bogosqrt(ptrdiff_t n)
{
ptrdiff_t r = 1;
while (r * r < n)
r <<= 1;
return r;
}
// Split content into lines, preserving the information about whether
// the file ended with a newline. Each element is one line WITHOUT its
// terminating '\n'.
struct FileLines {
std::vector<std::string_view> lines;
bool has_trailing_newline = true;
};
static FileLines split_file_lines(std::string_view content)
{
FileLines fl;
if (content.empty()) {
fl.has_trailing_newline = true; // empty file is fine
return fl;
}
fl.has_trailing_newline = (content.back() == '\n');
ptrdiff_t start = 0;
ptrdiff_t len = std::ssize(content);
for (ptrdiff_t i = 0; i < len; ++i) {
if (content[checked_cast<size_t>(i)] == '\n') {
fl.lines.push_back(content.substr(checked_cast<size_t>(start),
checked_cast<size_t>(i - start)));
start = i + 1;
}
}
// If there's content after the last newline (no trailing newline)
if (start < len) {
fl.lines.push_back(content.substr(checked_cast<size_t>(start),
checked_cast<size_t>(len - start)));
}
return fl;
}
// Myers diff algorithm.
// Returns a list of edit operations: 'E' (equal), 'D' (delete from old),
// 'I' (insert from new).
struct EditOp {
char type; // 'E', 'D', 'I'
ptrdiff_t old_idx; // index in old_lines (-1 for Insert)
ptrdiff_t new_idx; // index in new_lines (-1 for Delete)
};
// Backtrack through a Myers trace from (x,y) back to (0,0), building
// edit operations in reverse order. Returns ops in forward order.
static std::vector<EditOp> backtrack_trace(
const std::vector<std::vector<ptrdiff_t>> &trace,
ptrdiff_t final_d, ptrdiff_t x, ptrdiff_t y, ptrdiff_t offset)
{
std::vector<EditOp> ops;
for (ptrdiff_t d = final_d; d > 0; --d) {
const auto &prev_v = trace[checked_cast<size_t>(d)];
ptrdiff_t k = x - y;
ptrdiff_t prev_k;
if (k == -d || (k != d && prev_v[checked_cast<size_t>(offset + k - 1)] < prev_v[checked_cast<size_t>(offset + k + 1)])) {
prev_k = k + 1; // came from insert (down)
} else {
prev_k = k - 1; // came from delete (right)
}
ptrdiff_t prev_x = prev_v[checked_cast<size_t>(offset + prev_k)];
ptrdiff_t prev_y = prev_x - prev_k;
// Diagonal (equal lines) — add in reverse
while (x > prev_x && y > prev_y) {
--x;
--y;
ops.push_back({'E', x, y});
}
// The actual edit
if (x > prev_x) {
--x;
ops.push_back({'D', x, -1});
} else if (y > prev_y) {
--y;
ops.push_back({'I', -1, y});
}
}
// Remaining diagonal at d=0
while (x > 0 && y > 0) {
--x;
--y;
ops.push_back({'E', x, y});
}
std::ranges::reverse(ops);
return ops;
}
static std::vector<EditOp> myers_diff(
std::span<const std::string_view> old_lines,
std::span<const std::string_view> new_lines,
DiffAlgorithm algorithm = DiffAlgorithm::myers)
{
ptrdiff_t n = std::ssize(old_lines);
ptrdiff_t m = std::ssize(new_lines);
// Trivial cases
if (n == 0 && m == 0) {
return {};
}
if (n == 0) {
std::vector<EditOp> ops;
ops.reserve(checked_cast<size_t>(m));
for (ptrdiff_t j = 0; j < m; ++j)
ops.push_back({'I', -1, j});
return ops;
}
if (m == 0) {
std::vector<EditOp> ops;
ops.reserve(checked_cast<size_t>(n));
for (ptrdiff_t i = 0; i < n; ++i)
ops.push_back({'D', i, -1});
return ops;
}
// Myers' algorithm with linear-space trace recording.
// We store the V array for each D step to reconstruct the path.
ptrdiff_t max_d = n + m;
// V is indexed by k + offset where k ranges from -max_d to max_d
ptrdiff_t offset = max_d;
ptrdiff_t v_size = 2 * max_d + 1;
// Cost cap for myers mode (heuristic, matches libxdiff).
// minimal mode searches the full O(ND) space.
ptrdiff_t mxcost = max_d;
if (algorithm == DiffAlgorithm::myers) {
mxcost = bogosqrt(n + m);
if (mxcost < 256) mxcost = 256;
if (mxcost > max_d) mxcost = max_d;
}
// Store a copy of V for each d value to reconstruct the edit path
std::vector<std::vector<ptrdiff_t>> trace;
std::vector<ptrdiff_t> v(checked_cast<size_t>(v_size), -1);
v[checked_cast<size_t>(offset + 1)] = 0;
ptrdiff_t final_d = -1;
for (ptrdiff_t d = 0; d <= max_d; ++d) {
trace.push_back(v); // save state before this round
for (ptrdiff_t k = -d; k <= d; k += 2) {
ptrdiff_t x;
if (k == -d || (k != d && v[checked_cast<size_t>(offset + k - 1)] < v[checked_cast<size_t>(offset + k + 1)])) {
x = v[checked_cast<size_t>(offset + k + 1)]; // move down (insert)
} else {
x = v[checked_cast<size_t>(offset + k - 1)] + 1; // move right (delete)
}
ptrdiff_t y = x - k;
// Follow diagonal (equal lines)
while (x < n && y < m && old_lines[checked_cast<size_t>(x)] == new_lines[checked_cast<size_t>(y)]) {
++x;
++y;
}
v[checked_cast<size_t>(offset + k)] = x;
if (x >= n && y >= m) {
final_d = d;
goto found;
}
}
// Cost heuristic: if we've exceeded the budget, pick the
// furthest-reaching endpoint and construct a suboptimal script.
// This matches libxdiff's behavior for --diff-algorithm=myers.
if (d >= mxcost && d < max_d) {
// Find the diagonal with maximum progress (x + y)
ptrdiff_t best_x = -1, best_y = -1;
for (ptrdiff_t k = -d; k <= d; k += 2) {
ptrdiff_t x = v[checked_cast<size_t>(offset + k)];
if (x < 0) continue;
ptrdiff_t y = x - k;
if (x > n || y > m || y < 0) continue;
if (best_x < 0 || (x + y) > (best_x + best_y)) {
best_x = x;
best_y = y;
}
}
if (best_x >= 0) {
// Backtrack from the best endpoint to (0,0)
final_d = d;
auto ops = backtrack_trace(trace, final_d, best_x, best_y, offset);
// Recurse on the remaining portion with a fresh search
// budget, so matching lines past the cutoff are found.
auto tail_old = old_lines.subspan(checked_cast<size_t>(best_x));
auto tail_new = new_lines.subspan(checked_cast<size_t>(best_y));
auto tail_ops = myers_diff(tail_old, tail_new, algorithm);
// Adjust indices back to the original coordinate space
for (auto &op : tail_ops) {
if (op.old_idx >= 0) op.old_idx += best_x;
if (op.new_idx >= 0) op.new_idx += best_y;
ops.push_back(op);
}
return ops;
}
}
}
found:
return backtrack_trace(trace, final_d, n, m, offset);
}
// Patience diff algorithm.
//
// Anchors on lines that appear exactly once in each file, computes their
// longest increasing subsequence (LIS) via patience sorting, and recurses
// on the gaps between anchors. Falls back to Myers (minimal) when no
// unique common lines exist.
static std::vector<EditOp> patience_diff(
std::span<const std::string_view> old_lines,
std::span<const std::string_view> new_lines)
{
ptrdiff_t n = std::ssize(old_lines);
ptrdiff_t m = std::ssize(new_lines);
if (n == 0 && m == 0) return {};
if (n == 0) {
std::vector<EditOp> ops;
for (ptrdiff_t j = 0; j < m; ++j)
ops.push_back({'I', -1, j});
return ops;
}
if (m == 0) {
std::vector<EditOp> ops;
for (ptrdiff_t i = 0; i < n; ++i)
ops.push_back({'D', i, -1});
return ops;
}
// Step 1: Match common prefix and suffix.
ptrdiff_t prefix = 0;
while (prefix < n && prefix < m &&
old_lines[checked_cast<size_t>(prefix)] == new_lines[checked_cast<size_t>(prefix)])
++prefix;
ptrdiff_t suffix = 0;
while (suffix < (n - prefix) && suffix < (m - prefix) &&
old_lines[checked_cast<size_t>(n - 1 - suffix)] == new_lines[checked_cast<size_t>(m - 1 - suffix)])
++suffix;
ptrdiff_t inner_old_len = n - prefix - suffix;
ptrdiff_t inner_new_len = m - prefix - suffix;
// If prefix + suffix covers everything, no interior to diff.
if (inner_old_len <= 0 && inner_new_len <= 0) {
std::vector<EditOp> ops;
for (ptrdiff_t i = 0; i < n; ++i)
ops.push_back({'E', i, i});
return ops;
}
// Step 2: Find unique common lines in the interior.
// Map line content → (count_in_old, old_idx, count_in_new, new_idx).
struct LineInfo {
int old_count = 0;
ptrdiff_t old_idx = -1;
int new_count = 0;
ptrdiff_t new_idx = -1;
};
std::unordered_map<std::string_view, LineInfo> line_map;
for (ptrdiff_t i = 0; i < inner_old_len; ++i) {
auto &info = line_map[old_lines[checked_cast<size_t>(prefix + i)]];
info.old_count++;
info.old_idx = i; // keeps last occurrence, but we only care when count==1
}
for (ptrdiff_t j = 0; j < inner_new_len; ++j) {
auto &info = line_map[new_lines[checked_cast<size_t>(prefix + j)]];
info.new_count++;
info.new_idx = j;
}
// Collect unique matches, sorted by old_idx (natural insertion order
// from scanning, but we sort explicitly to be safe).
struct Match { ptrdiff_t old_idx; ptrdiff_t new_idx; };
std::vector<Match> unique_matches;
for (const auto &[line, info] : line_map) {
if (info.old_count == 1 && info.new_count == 1) {
unique_matches.push_back({info.old_idx, info.new_idx});
}
}
std::ranges::sort(unique_matches, {}, &Match::old_idx);
// Step 3: LIS of new_idx values via patience sorting.
// Each pile stores (new_idx, back_pointer into flat list).
struct Card {
ptrdiff_t new_idx;
ptrdiff_t old_idx;
ptrdiff_t back; // index into cards[] of predecessor, or -1
};
std::vector<Card> cards;
std::vector<ptrdiff_t> pile_tops; // indices into cards[] for top of each pile
for (const auto &match : unique_matches) {
// Binary search: find leftmost pile whose top new_idx >= match.new_idx
ptrdiff_t lo = 0, hi = std::ssize(pile_tops);
while (lo < hi) {
ptrdiff_t mid = lo + (hi - lo) / 2;
if (cards[checked_cast<size_t>(pile_tops[checked_cast<size_t>(mid)])].new_idx < match.new_idx)
lo = mid + 1;
else
hi = mid;
}
ptrdiff_t back = (lo > 0) ? pile_tops[checked_cast<size_t>(lo - 1)] : ptrdiff_t{-1};
ptrdiff_t card_idx = std::ssize(cards);
cards.push_back({match.new_idx, match.old_idx, back});
if (lo == std::ssize(pile_tops))
pile_tops.push_back(card_idx);
else
pile_tops[checked_cast<size_t>(lo)] = card_idx;
}
// Reconstruct LIS by walking back-pointers from the last pile's top.
std::vector<Match> anchors;
if (!pile_tops.empty()) {
ptrdiff_t idx = pile_tops.back();
while (idx >= 0) {
const auto &c = cards[checked_cast<size_t>(idx)];
anchors.push_back({c.old_idx, c.new_idx});
idx = c.back;
}
std::ranges::reverse(anchors);
}
// If no unique anchors found, fall back to Myers (minimal) on the interior.
if (anchors.empty()) {
auto inner_old = old_lines.subspan(checked_cast<size_t>(prefix),
checked_cast<size_t>(inner_old_len));
auto inner_new = new_lines.subspan(checked_cast<size_t>(prefix),
checked_cast<size_t>(inner_new_len));
auto inner_ops = myers_diff(inner_old, inner_new, DiffAlgorithm::minimal);
// Assemble: prefix + inner + suffix
std::vector<EditOp> ops;
for (ptrdiff_t i = 0; i < prefix; ++i)
ops.push_back({'E', i, i});
for (auto &op : inner_ops) {
if (op.old_idx >= 0) op.old_idx += prefix;
if (op.new_idx >= 0) op.new_idx += prefix;
ops.push_back(op);
}
for (ptrdiff_t i = 0; i < suffix; ++i)
ops.push_back({'E', n - suffix + i, m - suffix + i});
return ops;
}
// Step 4: Recurse on gaps between anchors.
std::vector<EditOp> ops;
// Emit prefix
for (ptrdiff_t i = 0; i < prefix; ++i)
ops.push_back({'E', i, i});
ptrdiff_t prev_old = 0; // interior-relative
ptrdiff_t prev_new = 0;
for (const auto &anchor : anchors) {
// Gap before this anchor
ptrdiff_t gap_old_len = anchor.old_idx - prev_old;
ptrdiff_t gap_new_len = anchor.new_idx - prev_new;
if (gap_old_len > 0 || gap_new_len > 0) {
auto gap_old = old_lines.subspan(checked_cast<size_t>(prefix + prev_old),
checked_cast<size_t>(gap_old_len));
auto gap_new = new_lines.subspan(checked_cast<size_t>(prefix + prev_new),
checked_cast<size_t>(gap_new_len));
auto gap_ops = patience_diff(gap_old, gap_new);
for (auto &op : gap_ops) {
if (op.old_idx >= 0) op.old_idx += prefix + prev_old;
if (op.new_idx >= 0) op.new_idx += prefix + prev_new;
ops.push_back(op);
}
}
// Emit the anchor as an equal match
ops.push_back({'E', prefix + anchor.old_idx, prefix + anchor.new_idx});
prev_old = anchor.old_idx + 1;
prev_new = anchor.new_idx + 1;
}
// Gap after last anchor
ptrdiff_t tail_old_len = inner_old_len - prev_old;
ptrdiff_t tail_new_len = inner_new_len - prev_new;
if (tail_old_len > 0 || tail_new_len > 0) {
auto tail_old = old_lines.subspan(checked_cast<size_t>(prefix + prev_old),
checked_cast<size_t>(tail_old_len));
auto tail_new = new_lines.subspan(checked_cast<size_t>(prefix + prev_new),
checked_cast<size_t>(tail_new_len));
auto tail_ops = patience_diff(tail_old, tail_new);
for (auto &op : tail_ops) {
if (op.old_idx >= 0) op.old_idx += prefix + prev_old;
if (op.new_idx >= 0) op.new_idx += prefix + prev_new;
ops.push_back(op);
}
}
// Emit suffix
for (ptrdiff_t i = 0; i < suffix; ++i)
ops.push_back({'E', n - suffix + i, m - suffix + i});
return ops;
}
// Histogram diff algorithm.
//
// Extends patience diff by relaxing the strict uniqueness requirement.
// Instead of only anchoring on lines unique in both files, histogram diff
// builds an occurrence-count index of the old file and finds the longest
// contiguous matching block anchored at the lowest-occurrence line. It
// then recurses on the regions before and after the block. Falls back to
// Myers (minimal) when no suitable anchor exists.
static constexpr int MAX_CHAIN_LENGTH = 64;
static constexpr int MAX_RECURSION = 1024; // same as Git's MAX_CNT_RECURSIVE
static std::vector<EditOp> histogram_diff_impl(
std::span<const std::string_view> old_lines,
std::span<const std::string_view> new_lines,
int depth)
{
ptrdiff_t n = std::ssize(old_lines);
ptrdiff_t m = std::ssize(new_lines);
if (n == 0 && m == 0) return {};
if (n == 0) {
std::vector<EditOp> ops;
for (ptrdiff_t j = 0; j < m; ++j)
ops.push_back({'I', -1, j});
return ops;
}
if (m == 0) {
std::vector<EditOp> ops;
for (ptrdiff_t i = 0; i < n; ++i)
ops.push_back({'D', i, -1});
return ops;
}
// Step 1: Match common prefix and suffix.
ptrdiff_t prefix = 0;
while (prefix < n && prefix < m &&
old_lines[checked_cast<size_t>(prefix)] == new_lines[checked_cast<size_t>(prefix)])
++prefix;
ptrdiff_t suffix = 0;
while (suffix < (n - prefix) && suffix < (m - prefix) &&
old_lines[checked_cast<size_t>(n - 1 - suffix)] == new_lines[checked_cast<size_t>(m - 1 - suffix)])
++suffix;
ptrdiff_t inner_old_len = n - prefix - suffix;
ptrdiff_t inner_new_len = m - prefix - suffix;
if (inner_old_len <= 0 && inner_new_len <= 0) {
std::vector<EditOp> ops;
for (ptrdiff_t i = 0; i < n; ++i)
ops.push_back({'E', i, i});
return ops;
}
// If recursion is too deep, fall back to Myers to avoid O(N²) behavior
// on files with many unique lines (where each split removes only one line).
if (depth >= MAX_RECURSION) {
auto inner_old = old_lines.subspan(checked_cast<size_t>(prefix),
checked_cast<size_t>(inner_old_len));
auto inner_new = new_lines.subspan(checked_cast<size_t>(prefix),
checked_cast<size_t>(inner_new_len));
auto inner_ops = myers_diff(inner_old, inner_new, DiffAlgorithm::minimal);
std::vector<EditOp> ops;
for (ptrdiff_t i = 0; i < prefix; ++i)
ops.push_back({'E', i, i});
for (auto &op : inner_ops) {
if (op.old_idx >= 0) op.old_idx += prefix;
if (op.new_idx >= 0) op.new_idx += prefix;
ops.push_back(op);
}
for (ptrdiff_t i = 0; i < suffix; ++i)
ops.push_back({'E', n - suffix + i, m - suffix + i});
return ops;
}
// Step 2: Build histogram of old interior lines.
// Map line content → (occurrence count, list of positions in old).
// Also build a parallel count array for O(1) lookup during extension.
struct HistEntry {
int count = 0;
std::vector<ptrdiff_t> positions; // interior-relative positions in old
};
std::unordered_map<std::string_view, HistEntry> hist;
std::vector<int> old_count(checked_cast<size_t>(inner_old_len));
for (ptrdiff_t i = 0; i < inner_old_len; ++i) {
auto &entry = hist[old_lines[checked_cast<size_t>(prefix + i)]];
entry.count++;
entry.positions.push_back(i);
}
for (ptrdiff_t i = 0; i < inner_old_len; ++i) {
auto it = hist.find(old_lines[checked_cast<size_t>(prefix + i)]);
old_count[checked_cast<size_t>(i)] = it->second.count;
}
// Step 3: Scan new interior lines to find the best contiguous matching block.
// Best block: lowest min-occurrence-count, then longest.
//
// Key optimizations vs. naive approach:
// (a) Skip lines in B whose A-count exceeds the current best threshold.
// (b) Skip A-occurrences whose count exceeds the threshold.
// (c) After extending a block forward, advance j past the extension so
// positions already covered are not re-scanned.
// (d) Use precomputed old_count[] instead of hash lookups during extension.
ptrdiff_t best_old_start = -1, best_new_start = -1, best_len = 0;
int best_threshold = MAX_CHAIN_LENGTH + 1;
for (ptrdiff_t j = 0; j < inner_new_len; ) {
auto it = hist.find(new_lines[checked_cast<size_t>(prefix + j)]);
if (it == hist.end()) { ++j; continue; }
const auto &entry = it->second;
if (entry.count > MAX_CHAIN_LENGTH || entry.count > best_threshold) {
++j; continue; // (a) can't improve on current best
}
ptrdiff_t j_advance = 1; // how far to advance j after this iteration
for (ptrdiff_t pi = 0; pi < std::ssize(entry.positions); ++pi) {
ptrdiff_t oi = entry.positions[checked_cast<size_t>(pi)];
// Extend backwards
ptrdiff_t back = 0;
int min_occ = entry.count;
while (oi - back - 1 >= 0 && j - back - 1 >= 0 &&
old_lines[checked_cast<size_t>(prefix + oi - back - 1)] ==
new_lines[checked_cast<size_t>(prefix + j - back - 1)]) {
++back;
int occ = old_count[checked_cast<size_t>(oi - back)];
if (occ < min_occ) min_occ = occ;
}
// Extend forwards (past the initial match at (oi, j))
ptrdiff_t fwd = 0;
while (oi + fwd + 1 < inner_old_len && j + fwd + 1 < inner_new_len &&
old_lines[checked_cast<size_t>(prefix + oi + fwd + 1)] ==
new_lines[checked_cast<size_t>(prefix + j + fwd + 1)]) {
++fwd;
int occ = old_count[checked_cast<size_t>(oi + fwd)];
if (occ < min_occ) min_occ = occ;
}
ptrdiff_t block_len = back + 1 + fwd;
ptrdiff_t block_old_start = oi - back;
ptrdiff_t block_new_start = j - back;
// (c) Skip j past forward extension to avoid re-scanning
if (fwd + 1 > j_advance)
j_advance = fwd + 1;
// Accept if lower occurrence threshold, or same threshold but longer
if (min_occ < best_threshold ||
(min_occ == best_threshold && block_len > best_len)) {
best_threshold = min_occ;
best_old_start = block_old_start;
best_new_start = block_new_start;
best_len = block_len;
}
}
j += j_advance;
}
// Step 4: If no block found, fall back to Myers (minimal).
auto inner_old = old_lines.subspan(checked_cast<size_t>(prefix),
checked_cast<size_t>(inner_old_len));
auto inner_new = new_lines.subspan(checked_cast<size_t>(prefix),
checked_cast<size_t>(inner_new_len));
if (best_len == 0) {
auto inner_ops = myers_diff(inner_old, inner_new, DiffAlgorithm::minimal);
std::vector<EditOp> ops;
for (ptrdiff_t i = 0; i < prefix; ++i)
ops.push_back({'E', i, i});
for (auto &op : inner_ops) {
if (op.old_idx >= 0) op.old_idx += prefix;
if (op.new_idx >= 0) op.new_idx += prefix;
ops.push_back(op);
}
for (ptrdiff_t i = 0; i < suffix; ++i)
ops.push_back({'E', n - suffix + i, m - suffix + i});
return ops;
}
// Step 5: Recurse on regions before and after the matching block.
std::vector<EditOp> ops;
// Emit prefix
for (ptrdiff_t i = 0; i < prefix; ++i)
ops.push_back({'E', i, i});
// Region before the block
if (best_old_start > 0 || best_new_start > 0) {
auto before_old = old_lines.subspan(checked_cast<size_t>(prefix),
checked_cast<size_t>(best_old_start));
auto before_new = new_lines.subspan(checked_cast<size_t>(prefix),
checked_cast<size_t>(best_new_start));
auto before_ops = histogram_diff_impl(before_old, before_new, depth + 1);
for (auto &op : before_ops) {
if (op.old_idx >= 0) op.old_idx += prefix;
if (op.new_idx >= 0) op.new_idx += prefix;
ops.push_back(op);
}
}
// Emit the matching block as Equal ops
for (ptrdiff_t k = 0; k < best_len; ++k) {
ops.push_back({'E', prefix + best_old_start + k,
prefix + best_new_start + k});
}
// Region after the block
ptrdiff_t after_old_start = best_old_start + best_len;
ptrdiff_t after_new_start = best_new_start + best_len;
ptrdiff_t after_old_len = inner_old_len - after_old_start;
ptrdiff_t after_new_len = inner_new_len - after_new_start;
if (after_old_len > 0 || after_new_len > 0) {
auto after_old = old_lines.subspan(checked_cast<size_t>(prefix + after_old_start),
checked_cast<size_t>(after_old_len));
auto after_new = new_lines.subspan(checked_cast<size_t>(prefix + after_new_start),
checked_cast<size_t>(after_new_len));
auto after_ops = histogram_diff_impl(after_old, after_new, depth + 1);
for (auto &op : after_ops) {
if (op.old_idx >= 0) op.old_idx += prefix + after_old_start;
if (op.new_idx >= 0) op.new_idx += prefix + after_new_start;
ops.push_back(op);
}
}
// Emit suffix
for (ptrdiff_t i = 0; i < suffix; ++i)
ops.push_back({'E', n - suffix + i, m - suffix + i});
return ops;
}
static std::vector<EditOp> histogram_diff(
std::span<const std::string_view> old_lines,
std::span<const std::string_view> new_lines)
{
return histogram_diff_impl(old_lines, new_lines, 0);
}
// A hunk groups consecutive edits with surrounding context lines.
struct Hunk {
ptrdiff_t old_start; // 1-based
ptrdiff_t old_count;
ptrdiff_t new_start; // 1-based
ptrdiff_t new_count;
std::vector<EditOp> ops; // the operations in this hunk (including context)
};
static std::vector<Hunk> build_hunks(const std::vector<EditOp> &ops,
ptrdiff_t context_lines)
{
std::vector<Hunk> hunks;
if (ops.empty()) return hunks;
// Find ranges of change (non-Equal) ops
struct ChangeRange { ptrdiff_t first; ptrdiff_t last; }; // inclusive indices into ops
std::vector<ChangeRange> changes;
for (ptrdiff_t i = 0; i < std::ssize(ops); ++i) {
if (ops[checked_cast<size_t>(i)].type != 'E') {
if (changes.empty() || i > changes.back().last + 1) {
changes.push_back({i, i});
} else {
changes.back().last = i;
}
}
}
if (changes.empty()) return hunks;
// Merge change ranges that overlap when context is added
std::vector<ChangeRange> merged;
merged.push_back(changes[0]);
for (ptrdiff_t i = 1; i < std::ssize(changes); ++i) {
// If the context windows overlap or are adjacent, merge
if (changes[checked_cast<size_t>(i)].first - merged.back().last <= 2 * context_lines) {
merged.back().last = changes[checked_cast<size_t>(i)].last;
} else {
merged.push_back(changes[checked_cast<size_t>(i)]);
}
}
// Build hunks from merged ranges
ptrdiff_t total_ops = std::ssize(ops);
for (const auto &range : merged) {
ptrdiff_t hunk_start = std::max(ptrdiff_t{0}, range.first - context_lines);
ptrdiff_t hunk_end = std::min(total_ops - 1, range.last + context_lines);
Hunk h;
h.ops.assign(ops.begin() + hunk_start, ops.begin() + hunk_end + 1);
// Compute old_start, old_count, new_start, new_count
h.old_count = 0;
h.new_count = 0;
h.old_start = 0;
h.new_start = 0;
bool first_old = true, first_new = true;
for (const auto &op : h.ops) {
if (op.type == 'E') {
if (first_old) { h.old_start = op.old_idx + 1; first_old = false; }
if (first_new) { h.new_start = op.new_idx + 1; first_new = false; }
h.old_count++;
h.new_count++;
} else if (op.type == 'D') {
if (first_old) { h.old_start = op.old_idx + 1; first_old = false; }
h.old_count++;
} else { // 'I'
if (first_new) { h.new_start = op.new_idx + 1; first_new = false; }
h.new_count++;
}
}
// If a side has no lines in the hunk (pure insert or pure delete),
// find the position from the preceding ops in the full ops list.
if (first_old) {
// Pure insert: old_start = last old line before insertion point
for (ptrdiff_t i = hunk_start - 1; i >= 0; --i) {
auto &prev = ops[checked_cast<size_t>(i)];
if (prev.old_idx >= 0) {
h.old_start = prev.old_idx + 1; // 1-based
break;
}
}
}
if (first_new) {
// Pure delete: new_start = last new line before deletion point
for (ptrdiff_t i = hunk_start - 1; i >= 0; --i) {
auto &prev = ops[checked_cast<size_t>(i)];
if (prev.new_idx >= 0) {
h.new_start = prev.new_idx + 1; // 1-based
break;
}
}
}
hunks.push_back(std::move(h));
}
return hunks;
}
// Format unified diff output
static std::string format_unified(
std::span<const std::string_view> old_lines,
std::span<const std::string_view> new_lines,
bool old_has_trailing_nl,
bool new_has_trailing_nl,
const std::vector<Hunk> &hunks,
std::string_view old_label,
std::string_view new_label)
{
std::string result;
// File headers
result += "--- ";
result += old_label;
result += '\n';
result += "+++ ";
result += new_label;
result += '\n';
ptrdiff_t old_total = std::ssize(old_lines);
ptrdiff_t new_total = std::ssize(new_lines);
for (const auto &hunk : hunks) {
// Hunk header: @@ -old_start[,old_count] +new_start[,new_count] @@
if (hunk.old_count == 1 && hunk.new_count == 1) {
result += std::format("@@ -{} +{} @@\n",
hunk.old_start, hunk.new_start);
} else if (hunk.old_count == 1) {
result += std::format("@@ -{} +{},{} @@\n",
hunk.old_start, hunk.new_start, hunk.new_count);
} else if (hunk.new_count == 1) {
result += std::format("@@ -{},{} +{} @@\n",
hunk.old_start, hunk.old_count, hunk.new_start);
} else {
result += std::format("@@ -{},{} +{},{} @@\n",
hunk.old_start, hunk.old_count,
hunk.new_start, hunk.new_count);
}
// Hunk body
for (const auto &op : hunk.ops) {
if (op.type == 'E') {
bool last_old = (op.old_idx == old_total - 1);
bool last_new = (op.new_idx == new_total - 1);
bool old_need_annot = last_old && !old_has_trailing_nl;
bool new_need_annot = last_new && !new_has_trailing_nl;
// When the trailing-newline annotation differs between
// sides, emit as D+I so each gets its own marker.
if (old_need_annot != new_need_annot) {
result += '-';
result += old_lines[checked_cast<size_t>(op.old_idx)];
result += '\n';
if (!old_has_trailing_nl) {
result += "\\ No newline at end of file\n";
}
result += '+';
result += new_lines[checked_cast<size_t>(op.new_idx)];
result += '\n';
if (!new_has_trailing_nl) {
result += "\\ No newline at end of file\n";
}
} else {
result += ' ';
result += old_lines[checked_cast<size_t>(op.old_idx)];
result += '\n';
if (last_old && !old_has_trailing_nl &&
last_new && !new_has_trailing_nl) {
result += "\\ No newline at end of file\n";
}
}
} else if (op.type == 'D') {
result += '-';
result += old_lines[checked_cast<size_t>(op.old_idx)];
result += '\n';
// Check if this is the last old line with no trailing newline
if (op.old_idx == old_total - 1 && !old_has_trailing_nl) {
result += "\\ No newline at end of file\n";
}
} else { // 'I'
result += '+';
result += new_lines[checked_cast<size_t>(op.new_idx)];
result += '\n';
// Check if this is the last new line with no trailing newline
if (op.new_idx == new_total - 1 && !new_has_trailing_nl) {
result += "\\ No newline at end of file\n";
}
}
}
}
return result;
}
// Format context diff output
static std::string format_context(
std::span<const std::string_view> old_lines,
std::span<const std::string_view> new_lines,
bool old_has_trailing_nl,
bool new_has_trailing_nl,
const std::vector<Hunk> &hunks,
std::string_view old_label,
std::string_view new_label)
{
std::string result;
// File headers
result += "*** ";
result += old_label;
result += '\n';
result += "--- ";
result += new_label;
result += '\n';
ptrdiff_t old_total = std::ssize(old_lines);
ptrdiff_t new_total = std::ssize(new_lines);
for (const auto &hunk : hunks) {
result += "***************\n";
// Classify each edit group: adjacent D and I runs form "changes" (! prefix)
// We need to build old-side and new-side lines with proper prefixes.
struct SideLine { char prefix; std::string_view text; bool no_newline; };
std::vector<SideLine> old_side, new_side;
ptrdiff_t num_ops = std::ssize(hunk.ops);
for (ptrdiff_t k = 0; k < num_ops; ) {
const auto &op = hunk.ops[checked_cast<size_t>(k)];
if (op.type == 'E') {
bool onl = (op.old_idx == old_total - 1 && !old_has_trailing_nl &&
op.new_idx == new_total - 1 && !new_has_trailing_nl);
old_side.push_back({' ', old_lines[checked_cast<size_t>(op.old_idx)], onl});
new_side.push_back({' ', new_lines[checked_cast<size_t>(op.new_idx)], onl});
++k;
} else {
// Collect consecutive D then I runs
ptrdiff_t ds = k;
while (k < num_ops && hunk.ops[checked_cast<size_t>(k)].type == 'D') ++k;
ptrdiff_t de = k; // exclusive
while (k < num_ops && hunk.ops[checked_cast<size_t>(k)].type == 'I') ++k;
ptrdiff_t ie = k; // exclusive
bool is_change = (de > ds && ie > de);
for (ptrdiff_t j = ds; j < de; ++j) {
auto &dop = hunk.ops[checked_cast<size_t>(j)];
bool onl = (dop.old_idx == old_total - 1 && !old_has_trailing_nl);
old_side.push_back({is_change ? '!' : '-',
old_lines[checked_cast<size_t>(dop.old_idx)], onl});
}
for (ptrdiff_t j = de; j < ie; ++j) {
auto &iop = hunk.ops[checked_cast<size_t>(j)];
bool onl = (iop.new_idx == new_total - 1 && !new_has_trailing_nl);
new_side.push_back({is_change ? '!' : '+',
new_lines[checked_cast<size_t>(iop.new_idx)], onl});
}
}
}
// Old range header
ptrdiff_t oe = hunk.old_count == 0 ? hunk.old_start : hunk.old_start + hunk.old_count - 1;
result += std::format("*** {},{} ****\n", hunk.old_start, oe);
// Print old-side lines only if there are changes (not just context)
bool has_old_changes = false;
for (const auto &sl : old_side) {
if (sl.prefix != ' ') { has_old_changes = true; break; }
}
if (has_old_changes) {
for (const auto &sl : old_side) {
result += sl.prefix;
result += ' ';
result += sl.text;
result += '\n';
if (sl.no_newline) {
result += "\\ No newline at end of file\n";
}
}
}
// New range header
ptrdiff_t ne = hunk.new_count == 0 ? hunk.new_start : hunk.new_start + hunk.new_count - 1;
result += std::format("--- {},{} ----\n", hunk.new_start, ne);
// Print new-side lines only if there are changes
bool has_new_changes = false;
for (const auto &sl : new_side) {
if (sl.prefix != ' ') { has_new_changes = true; break; }
}
if (has_new_changes) {
for (const auto &sl : new_side) {
result += sl.prefix;
result += ' ';
result += sl.text;
result += '\n';
if (sl.no_newline) {
result += "\\ No newline at end of file\n";
}
}
}
}
return result;
}
DiffResult builtin_diff(std::string_view old_path, std::string_view new_path,
int context_lines,
std::string_view old_label, std::string_view new_label,
DiffFormat format,
DiffAlgorithm algorithm,
std::map<std::string, std::string> *fs)
{
// Read files — treat /dev/null or non-existent as empty
std::string old_content, new_content;
bool old_is_null = (old_path == "/dev/null" || old_path.empty());
bool new_is_null = (new_path == "/dev/null" || new_path.empty());
auto fs_exists = [&](std::string_view p) -> bool {
if (fs) return fs->contains(std::string(p));
return file_exists(p);
};
auto fs_read = [&](std::string_view p) -> std::string {
if (fs) {
auto it = fs->find(std::string(p));
return it != fs->end() ? it->second : std::string{};
}
return read_file(p);
};
if (!old_is_null && fs_exists(old_path)) {
old_content = fs_read(old_path);
}
if (!new_is_null && fs_exists(new_path)) {
new_content = fs_read(new_path);
}
// Split into lines
auto old_fl = split_file_lines(old_content);
auto new_fl = split_file_lines(new_content);
// Run diff algorithm
std::vector<EditOp> ops;
if (algorithm == DiffAlgorithm::patience)
ops = patience_diff(old_fl.lines, new_fl.lines);
else if (algorithm == DiffAlgorithm::histogram)
ops = histogram_diff(old_fl.lines, new_fl.lines);
else
ops = myers_diff(old_fl.lines, new_fl.lines, algorithm);
// Check if there are any differences
bool has_diff = false;
for (const auto &op : ops) {
if (op.type != 'E') { has_diff = true; break; }
}
// Also check trailing newline difference
if (!has_diff && !old_fl.lines.empty() &&
old_fl.has_trailing_newline != new_fl.has_trailing_newline) {
has_diff = true;
}
if (!has_diff) {
return {0, ""};
}
// When trailing newlines differ the last line must appear as a D+I
// pair (not a context 'E') so each side gets the right "\ No newline"
// annotation. Replace the trailing 'E' with D+I before building hunks
// so that build_hunks sees a real change and includes it in a hunk.
if (!old_fl.lines.empty() &&
old_fl.has_trailing_newline != new_fl.has_trailing_newline) {
// Find the last 'E' op that covers the final line of both files
for (ptrdiff_t i = std::ssize(ops) - 1; i >= 0; --i) {
auto &op = ops[checked_cast<size_t>(i)];
if (op.type == 'E' &&
op.old_idx == std::ssize(old_fl.lines) - 1 &&
op.new_idx == std::ssize(new_fl.lines) - 1) {
// Replace with D then I
EditOp d_op{'D', op.old_idx, -1};
EditOp i_op{'I', -1, op.new_idx};
ops[checked_cast<size_t>(i)] = d_op;
ops.insert(ops.begin() + i + 1, i_op);
break;
}
if (op.type == 'E') break; // only check the last equal op
}
}
// Use labels or default to paths
std::string old_lbl = old_label.empty() ? std::string(old_path) : std::string(old_label);
std::string new_lbl = new_label.empty() ? std::string(new_path) : std::string(new_label);
// Build hunks
auto hunks = build_hunks(ops, context_lines);
std::string output;
if (format == DiffFormat::context) {
output = format_context(old_fl.lines, new_fl.lines,
old_fl.has_trailing_newline,
new_fl.has_trailing_newline,
hunks, old_lbl, new_lbl);
} else {
output = format_unified(old_fl.lines, new_fl.lines,
old_fl.has_trailing_newline,
new_fl.has_trailing_newline,
hunks, old_lbl, new_lbl);
}
return {1, std::move(output)};
}
// === src/patch.cpp ===
// This is free and unencumbered software released into the public domain.
//
// Built-in patch engine for applying unified diffs.
// Implements spiral search with offset tracking, fuzz matching,
// reverse application, merge conflict markers, and reject files.
#include <algorithm>
#include <cstdio>
#include <cstdlib>
// ── Patch parsing data structures ──────────────────────────────────────
struct PatchHunk {
ptrdiff_t old_start = 0; // 1-based line from @@ header
ptrdiff_t old_count = 0;
ptrdiff_t new_start = 0;
ptrdiff_t new_count = 0;
std::vector<std::string> lines; // prefixed with ' ', '+', '-'
// Flags for "\ No newline at end of file" on old/new side
bool old_no_newline = false;
bool new_no_newline = false;
};
struct PatchFile {
std::string old_path;
std::string new_path;
std::string target_path; // after strip-level
bool is_creation = false; // old = /dev/null
bool is_deletion = false; // new = /dev/null
std::vector<PatchHunk> hunks;
};
// ── Path stripping ─────────────────────────────────────────────────────
// Strip N leading path components. Adjacent slashes count as one separator.
static std::string strip_path(std::string_view path, int strip)
{
if (strip < 0) return std::string(path);
std::string_view p = path;
for (int i = 0; i < strip && !p.empty(); ++i) {
// Skip to next slash
ptrdiff_t slash = str_find(p, '/');
if (slash < 0) {
// No more slashes — strip everything
return std::string(p);
}
p = p.substr(checked_cast<size_t>(slash) + 1);
// Skip consecutive slashes
while (!p.empty() && p[0] == '/') p = p.substr(1);
}
return std::string(p);
}
// Extract filename from a --- or +++ header line.
// Strips trailing tab+timestamp if present.
static std::string extract_path(std::string_view line)
{
// line is everything after "--- " or "+++ "
std::string_view rest = line;
ptrdiff_t tab = str_find(rest, '\t');
if (tab >= 0) {
rest = rest.substr(0, checked_cast<size_t>(tab));
}
// Trim trailing whitespace
while (!rest.empty() && (rest.back() == ' ' || rest.back() == '\r')) {
rest = rest.substr(0, checked_cast<size_t>(std::ssize(rest) - 1));
}
return std::string(rest);
}
// ── Unified diff parser ────────────────────────────────────────────────
// Parse a complete unified diff into a list of per-file patch descriptions.
static std::vector<PatchFile> parse_patch(std::string_view text, int strip_level,
bool reverse)
{
std::vector<PatchFile> files;
auto lines = split_lines(text);
ptrdiff_t n = std::ssize(lines);
ptrdiff_t i = 0;
while (i < n) {
// Look for "--- " header
if (!lines[checked_cast<size_t>(i)].starts_with("--- ")) {
++i;
continue;
}
// Peek ahead for "+++ "
if (i + 1 >= n || !lines[checked_cast<size_t>(i + 1)].starts_with("+++ ")) {
++i;
continue;
}
PatchFile pf;
std::string raw_old = extract_path(std::string_view(lines[checked_cast<size_t>(i)]).substr(4));
std::string raw_new = extract_path(std::string_view(lines[checked_cast<size_t>(i + 1)]).substr(4));
if (reverse) {
std::swap(raw_old, raw_new);
}
pf.old_path = raw_old;
pf.new_path = raw_new;
pf.is_creation = (raw_old == "/dev/null");
pf.is_deletion = (raw_new == "/dev/null");
// Determine target path
// Prefer new path like GNU patch does for the common -p0 case
// where old has a .orig suffix (e.g., "--- f.txt.orig" / "+++ f.txt")
if (pf.is_creation) {
pf.target_path = strip_path(raw_new, strip_level);
} else if (pf.is_deletion) {
pf.target_path = strip_path(raw_old, strip_level);
} else {
std::string stripped_old = strip_path(raw_old, strip_level);
std::string stripped_new = strip_path(raw_new, strip_level);
// Use new path when old has .orig suffix, otherwise use
// the shorter path (GNU patch heuristic)
if (stripped_old.ends_with(".orig")) {
pf.target_path = stripped_new;
} else if (stripped_new.size() <= stripped_old.size()) {
pf.target_path = stripped_new;
} else {
pf.target_path = stripped_old;
}
}
i += 2; // skip --- and +++ lines
// Parse hunks
while (i < n && lines[checked_cast<size_t>(i)].starts_with("@@ ")) {
PatchHunk hunk;
// Parse @@ -old_start[,old_count] +new_start[,new_count] @@
std::string_view hdr = std::string_view(lines[checked_cast<size_t>(i)]);
ptrdiff_t at1 = str_find(hdr, '-', 3);
if (at1 < 0) { ++i; continue; }
// Parse old range
ptrdiff_t pos = at1 + 1;
ptrdiff_t comma = str_find(hdr, ',', pos);
ptrdiff_t space = str_find(hdr, ' ', pos);
ptrdiff_t plus_pos = str_find(hdr, '+', pos);
// Need '+' marker and a space or comma delimiter before it
if (plus_pos < 0) { ++i; continue; }
if (comma >= 0 && comma < plus_pos && plus_pos - comma >= 2) {
hunk.old_start = parse_int(hdr.substr(checked_cast<size_t>(pos), checked_cast<size_t>(comma - pos)));
hunk.old_count = parse_int(hdr.substr(checked_cast<size_t>(comma + 1), checked_cast<size_t>(plus_pos - comma - 2)));
} else if (space >= 0 && space < plus_pos) {
hunk.old_start = parse_int(hdr.substr(checked_cast<size_t>(pos), checked_cast<size_t>(space - pos)));
hunk.old_count = 1;
} else {
++i; continue;
}
// Parse new range
pos = plus_pos + 1;
comma = str_find(hdr, ',', pos);
ptrdiff_t end_at = str_find(hdr, ' ', pos);
if (end_at < 0) end_at = std::ssize(hdr);
if (end_at <= pos) { ++i; continue; }
if (comma >= 0 && comma < end_at) {
hunk.new_start = parse_int(hdr.substr(checked_cast<size_t>(pos), checked_cast<size_t>(comma - pos)));
hunk.new_count = parse_int(hdr.substr(checked_cast<size_t>(comma + 1), checked_cast<size_t>(end_at - comma - 1)));
} else {
hunk.new_start = parse_int(hdr.substr(checked_cast<size_t>(pos), checked_cast<size_t>(end_at - pos)));
hunk.new_count = 1;
}
if (reverse) {
std::swap(hunk.old_start, hunk.new_start);
std::swap(hunk.old_count, hunk.new_count);
}
++i; // skip @@ line
// Collect hunk body
ptrdiff_t old_seen = 0, new_seen = 0;
while (i < n) {
std::string_view ln = lines[checked_cast<size_t>(i)];
if (ln.starts_with("\\ No newline at end of file") ||
ln.starts_with("\\ no newline at end of file")) {
// Applies to the preceding line
if (!hunk.lines.empty()) {
// Prefixes are already swapped if reverse=true, so
// '-' is always the old side and '+' the new side.
char prev_prefix = hunk.lines.back()[0];
if (prev_prefix == '-')
hunk.old_no_newline = true;
else if (prev_prefix == '+')
hunk.new_no_newline = true;
else
hunk.old_no_newline = hunk.new_no_newline = true;
}
++i;
continue;
}
if (ln.empty()) {
// Empty line in diff = context line (space was stripped)
if (old_seen >= hunk.old_count && new_seen >= hunk.new_count) break;
std::string line_str = " ";
hunk.lines.push_back(line_str);
old_seen++;
new_seen++;
++i;
continue;
}
char prefix = ln[0];
if (prefix == ' ' || prefix == '-' || prefix == '+') {
std::string line_str(ln);
if (reverse) {
if (prefix == '-') line_str[0] = '+';
else if (prefix == '+') line_str[0] = '-';
}
char actual_prefix = line_str[0];
if (actual_prefix == ' ') {
if (old_seen >= hunk.old_count && new_seen >= hunk.new_count) break;
old_seen++;
new_seen++;
} else if (actual_prefix == '-') {
if (old_seen >= hunk.old_count) break;
old_seen++;
} else { // '+'
if (new_seen >= hunk.new_count) break;
new_seen++;
}
hunk.lines.push_back(std::move(line_str));
++i;
} else {
// Start of next file section or unknown line
break;
}
}
pf.hunks.push_back(std::move(hunk));
}
files.push_back(std::move(pf));
}
return files;
}
// ── Line-based file representation ─────────────────────────────────────
// Split file content into lines. Each line does NOT include its trailing '\n'.
// Returns whether the file had a trailing newline.
struct FileContent {
std::vector<std::string> lines;
bool has_trailing_newline = true;
bool crlf = false; // true if original file used \r\n line endings
};
static FileContent load_file_lines(std::string_view content)
{
FileContent fc;
if (content.empty()) {
fc.has_trailing_newline = true;
return fc;
}
fc.has_trailing_newline = (content.back() == '\n');
// Detect \r\n from the first line ending
auto first_lf = str_find(content, '\n');
if (first_lf > 0 && content[checked_cast<size_t>(first_lf - 1)] == '\r')
fc.crlf = true;
ptrdiff_t start = 0;
ptrdiff_t len = std::ssize(content);
for (ptrdiff_t i = 0; i < len; ++i) {
if (content[checked_cast<size_t>(i)] == '\n') {
ptrdiff_t end = i;
if (end > start && content[checked_cast<size_t>(end - 1)] == '\r')
--end;
fc.lines.emplace_back(content.substr(checked_cast<size_t>(start), checked_cast<size_t>(end - start)));
start = i + 1;
}
}
if (start < len) {
std::string tail(content.substr(checked_cast<size_t>(start), checked_cast<size_t>(len - start)));
if (!tail.empty() && tail.back() == '\r')
tail.pop_back();
fc.lines.push_back(std::move(tail));
}
return fc;
}
// ── Hunk matching ──────────────────────────────────────────────────────
// Extract the context+deletion lines (the "old" side pattern) from a hunk.
// Returns pairs of (line_text, is_context) for matching purposes.
struct PatternLine {
std::string_view text; // line content (without prefix)
bool is_context; // true = context line, false = deletion line
};
static std::vector<PatternLine> get_old_pattern(const PatchHunk &hunk)
{
std::vector<PatternLine> pattern;
for (const auto &line : hunk.lines) {
char prefix = line[0];
std::string_view text(line);
text = text.substr(1);
if (prefix == ' ') {
pattern.push_back({text, true});
} else if (prefix == '-') {
pattern.push_back({text, false});
}
// '+' lines are not part of the old-side pattern
}
return pattern;
}
// Count prefix and suffix context lines from the full hunk (including +/-
// lines). This gives the true context extent: prefix context is the number
// of ' ' lines before the first '+' or '-' line, and suffix context is the
// number of ' ' lines after the last '+' or '-' line.
struct HunkContext {
ptrdiff_t prefix = 0;
ptrdiff_t suffix = 0;
};
// Per-hunk fuzz amounts used when matching (for trimming during application).
struct HunkFuzz {
ptrdiff_t prefix = 0;
ptrdiff_t suffix = 0;
};
static HunkContext get_hunk_context(const PatchHunk &hunk)
{
HunkContext ctx;
for (const auto &line : hunk.lines) {
if (line[0] == ' ') ++ctx.prefix;
else break;
}
for (auto it = hunk.lines.rbegin(); it != hunk.lines.rend(); ++it) {
if ((*it)[0] == ' ') ++ctx.suffix;
else break;
}
return ctx;
}
// Try to match a hunk's old-side pattern against file lines starting at
// position `pos` (0-based), with `fuzz` context lines skipped at top/bottom.
// prefix_ctx/suffix_ctx are the real context extents from the full hunk.
// Returns true if the pattern matches.
static bool try_match(std::span<const std::string> file_lines,
ptrdiff_t pos,
const std::vector<PatternLine> &pattern,
int fuzz,
ptrdiff_t prefix_ctx,
ptrdiff_t suffix_ctx)
{
ptrdiff_t pat_len = std::ssize(pattern);
if (pat_len == 0) return true;
ptrdiff_t prefix_fuzz = std::min(static_cast<ptrdiff_t>(fuzz), prefix_ctx);
ptrdiff_t suffix_fuzz = std::min(static_cast<ptrdiff_t>(fuzz), suffix_ctx);
// Lines to match: skip prefix_fuzz from top, suffix_fuzz from bottom
ptrdiff_t match_start = prefix_fuzz;
ptrdiff_t match_end = pat_len - suffix_fuzz;
// Adjust file position: we start matching at pos + prefix_fuzz
ptrdiff_t file_pos = pos + prefix_fuzz;
ptrdiff_t file_len = std::ssize(file_lines);
for (ptrdiff_t j = match_start; j < match_end; ++j) {
if (file_pos < 0 || file_pos >= file_len) return false;
if (file_lines[checked_cast<size_t>(file_pos)] != pattern[checked_cast<size_t>(j)].text) return false;
++file_pos;
}
return true;
}
// Spiral search: find where a hunk matches in the file.
// Returns the 0-based file position, or -1 if not found.
// Updates cumulative_offset on success.
static ptrdiff_t locate_hunk(std::span<const std::string> file_lines,
const PatchHunk &hunk,
const std::vector<PatternLine> &pattern,
ptrdiff_t last_frozen_line,
ptrdiff_t cumulative_offset,
int max_fuzz)
{
ptrdiff_t file_len = std::ssize(file_lines);
ptrdiff_t pat_old_count = std::ssize(pattern);
// Get real prefix/suffix context from full hunk (not just old-side pattern)
auto ctx = get_hunk_context(hunk);
// First guess: hunk header's old_start (1-based) converted to 0-based + offset
ptrdiff_t first_guess = hunk.old_start - 1 + cumulative_offset;
// Clamp to valid range
ptrdiff_t max_pos = file_len - pat_old_count;
if (max_pos < 0) max_pos = 0;
for (int fuzz = 0; fuzz <= max_fuzz; ++fuzz) {
ptrdiff_t prefix_fuzz = std::min(static_cast<ptrdiff_t>(fuzz), ctx.prefix);
ptrdiff_t suffix_fuzz = std::min(static_cast<ptrdiff_t>(fuzz), ctx.suffix);
ptrdiff_t effective_pat_len = pat_old_count - prefix_fuzz - suffix_fuzz;
ptrdiff_t max_search = file_len - effective_pat_len;
if (effective_pat_len == 0) max_search = file_len; // empty pattern matches anywhere
// Try exact position first
if (first_guess >= 0 && first_guess <= max_search &&
first_guess > last_frozen_line - 1) {
if (try_match(file_lines, first_guess, pattern, fuzz, ctx.prefix, ctx.suffix)) {
return first_guess;
}
}
// Spiral outward
ptrdiff_t max_offset_forward = max_search - first_guess;
ptrdiff_t max_offset_backward = first_guess - last_frozen_line;
ptrdiff_t max_range = std::max(max_offset_forward, max_offset_backward);
if (max_range < 0) max_range = 0;
for (ptrdiff_t delta = 1; delta <= max_range; ++delta) {
// Try forward
ptrdiff_t pos = first_guess + delta;
if (pos >= 0 && pos <= max_search && pos > last_frozen_line - 1) {
if (try_match(file_lines, pos, pattern, fuzz, ctx.prefix, ctx.suffix)) {
return pos;
}
}
// Try backward
pos = first_guess - delta;
if (pos >= 0 && pos <= max_search && pos > last_frozen_line - 1) {
if (try_match(file_lines, pos, pattern, fuzz, ctx.prefix, ctx.suffix)) {
return pos;
}
}
}
}
return -1; // no match found
}
// ── Hunk application ───────────────────────────────────────────────────
// Get the new-side (replacement) lines from a hunk.
static std::vector<std::string_view> get_new_lines(const PatchHunk &hunk)
{
std::vector<std::string_view> result;
for (const auto &line : hunk.lines) {
char prefix = line[0];
if (prefix == ' ' || prefix == '+') {
result.push_back(std::string_view(line).substr(1));
}
}
return result;
}
// Build the output file content after applying all successfully matched hunks.
// hunks_positions[i] = 0-based file position where hunk i matched, or -1 if rejected.
// hunk_fuzz[i] = fuzz amounts used for hunk i (to trim context from both sides).
static std::string build_output(std::span<const std::string> file_lines,
bool has_trailing_newline,
const PatchFile &pf,
const std::vector<ptrdiff_t> &hunk_positions,
const std::vector<HunkFuzz> &hunk_fuzz)
{
std::string output;
ptrdiff_t file_len = std::ssize(file_lines);
ptrdiff_t last_copied = 0; // next line to copy from input
for (ptrdiff_t h = 0; h < std::ssize(pf.hunks); ++h) {
ptrdiff_t pos = hunk_positions[checked_cast<size_t>(h)];
if (pos < 0) continue; // rejected hunk, skip
const auto &hunk = pf.hunks[checked_cast<size_t>(h)];
auto pattern = get_old_pattern(hunk);
ptrdiff_t pat_len = std::ssize(pattern);
auto new_lines = get_new_lines(hunk);
// When fuzz was used, trim the fuzzed context lines from both sides.
// The fuzzed prefix/suffix context lines were not matched against the
// file, so we must not replace them.
auto fz = hunk_fuzz[checked_cast<size_t>(h)];
pos += fz.prefix;
pat_len -= fz.prefix + fz.suffix;
if (pat_len < 0) pat_len = 0;
ptrdiff_t new_start = fz.prefix;
ptrdiff_t new_end = std::ssize(new_lines) - fz.suffix;
if (new_end < new_start) new_end = new_start;
// Clamp to file bounds
if (pos > file_len) pos = file_len;
// Copy unchanged lines from last_copied to pos
for (ptrdiff_t j = last_copied; j < pos; ++j) {
output += file_lines[checked_cast<size_t>(j)];
output += '\n';
}
// Write replacement lines (trimmed by fuzz)
for (ptrdiff_t j = new_start; j < new_end; ++j) {
output += new_lines[checked_cast<size_t>(j)];
bool is_last_new_line = (j == new_end - 1);
if (is_last_new_line && fz.suffix == 0 && hunk.new_no_newline) {
// Don't add trailing newline (only when suffix not trimmed)
} else {
output += '\n';
}
}
last_copied = pos + pat_len;
if (last_copied > file_len) last_copied = file_len;
}
// Copy remaining lines
for (ptrdiff_t j = last_copied; j < file_len; ++j) {
output += file_lines[checked_cast<size_t>(j)];
if (j < file_len - 1) {
output += '\n';
} else {
// Last line: preserve original trailing newline status
// unless a hunk changed it
if (has_trailing_newline) {
output += '\n';
}
}
}
return output;
}
// ── Merge conflict markers ─────────────────────────────────────────────
// Build output with merge conflict markers for rejected hunks.
// Applies successful hunks normally, inserts conflict markers for failed ones.
static std::string build_merge_output(std::span<const std::string> file_lines,
bool has_trailing_newline,
const PatchFile &pf,
const std::vector<ptrdiff_t> &hunk_positions,
const std::vector<HunkFuzz> &hunk_fuzz,
std::string_view merge_style)
{
// For merge mode, we first apply successful hunks, then for rejected hunks
// we insert conflict markers at the hunk's expected position.
std::string output;
ptrdiff_t file_len = std::ssize(file_lines);
ptrdiff_t last_copied = 0;
// Process all hunks in order
for (ptrdiff_t h = 0; h < std::ssize(pf.hunks); ++h) {
const auto &hunk = pf.hunks[checked_cast<size_t>(h)];
ptrdiff_t pos = hunk_positions[checked_cast<size_t>(h)];
if (pos >= 0) {
// Successfully matched — apply normally
if (pos > file_len) pos = file_len;
auto pattern = get_old_pattern(hunk);
ptrdiff_t pat_len = std::ssize(pattern);
auto new_lines = get_new_lines(hunk);
// Trim fuzzed context lines
auto fz = hunk_fuzz[checked_cast<size_t>(h)];
pos += fz.prefix;
pat_len -= fz.prefix + fz.suffix;
if (pat_len < 0) pat_len = 0;
ptrdiff_t new_start = fz.prefix;
ptrdiff_t new_end = std::ssize(new_lines) - fz.suffix;
if (new_end < new_start) new_end = new_start;
if (pos > file_len) pos = file_len;
for (ptrdiff_t j = last_copied; j < pos; ++j) {
output += file_lines[checked_cast<size_t>(j)];
output += '\n';
}
for (ptrdiff_t j = new_start; j < new_end; ++j) {
output += new_lines[checked_cast<size_t>(j)];
bool is_last = (j == new_end - 1);
if (is_last && fz.suffix == 0 && hunk.new_no_newline) {
// no trailing newline
} else {
output += '\n';
}
}
last_copied = pos + pat_len;
if (last_copied > file_len) last_copied = file_len;
} else {
// Rejected — insert per-change conflict markers at expected position
ptrdiff_t expected = hunk.old_start - 1;
if (expected < last_copied) expected = last_copied;
if (expected > file_len) expected = file_len;
// Copy up to expected position
for (ptrdiff_t j = last_copied; j < expected; ++j) {
output += file_lines[checked_cast<size_t>(j)];
output += '\n';
}
// Walk through hunk lines, emitting context outside markers
// and changed regions inside markers.
ptrdiff_t file_pos = expected;
ptrdiff_t hi = 0;
ptrdiff_t hunk_len = std::ssize(hunk.lines);
while (hi < hunk_len) {
char prefix = hunk.lines[checked_cast<size_t>(hi)][0];
if (prefix == ' ') {
// Context line — emit the file's actual line
if (file_pos < file_len) {
output += file_lines[checked_cast<size_t>(file_pos)];
output += '\n';
++file_pos;
}
++hi;
} else {
// Changed region — collect contiguous -/+ lines
std::vector<std::string_view> old_lines, new_change;
while (hi < hunk_len && hunk.lines[checked_cast<size_t>(hi)][0] == '-') {
old_lines.push_back(std::string_view(hunk.lines[checked_cast<size_t>(hi)]).substr(1));
++hi;
}
while (hi < hunk_len && hunk.lines[checked_cast<size_t>(hi)][0] == '+') {
new_change.push_back(std::string_view(hunk.lines[checked_cast<size_t>(hi)]).substr(1));
++hi;
}
output += "<<<<<<<\n";
// Current file content for the old-side span
ptrdiff_t span = std::ssize(old_lines);
ptrdiff_t end = file_pos + span;
if (end > file_len) end = file_len;
for (ptrdiff_t j = file_pos; j < end; ++j) {
output += file_lines[checked_cast<size_t>(j)];
output += '\n';
}
if (merge_style == "diff3") {
output += "|||||||\n";
for (const auto &ol : old_lines) {
output += ol;
output += '\n';
}
}
output += "=======\n";
for (const auto &nl : new_change) {
output += nl;
output += '\n';
}
output += ">>>>>>>\n";
file_pos = end;
}
}
last_copied = file_pos;
}
}
// Copy remaining
for (ptrdiff_t j = last_copied; j < file_len; ++j) {
output += file_lines[checked_cast<size_t>(j)];
if (j < file_len - 1) {
output += '\n';
} else if (has_trailing_newline) {
output += '\n';
}
}
return output;
}
// ── Reject file generation ─────────────────────────────────────────────
// Format rejected hunks as a unified diff .rej file.
static std::string format_rejects(const PatchFile &pf,
const std::vector<bool> &rejected)
{
std::string result;
bool has_any = false;
for (ptrdiff_t h = 0; h < std::ssize(pf.hunks); ++h) {
if (!rejected[checked_cast<size_t>(h)]) continue;
const auto &hunk = pf.hunks[checked_cast<size_t>(h)];
if (!has_any) {
// Write file headers
result += "--- ";
result += pf.old_path;
result += '\n';
result += "+++ ";
result += pf.new_path;
result += '\n';
has_any = true;
}
// Write hunk header
result += std::format("@@ -{},{} +{},{} @@\n",
hunk.old_start, hunk.old_count,
hunk.new_start, hunk.new_count);
// Write hunk lines
for (const auto &line : hunk.lines) {
result += line;
result += '\n';
}
if (hunk.old_no_newline) {
result += "\\ No newline at end of file\n";
}
}
return result;
}
// ── Main patch engine ──────────────────────────────────────────────────
PatchResult builtin_patch(std::string_view patch_text, const PatchOptions &opts)
{
PatchResult result;
result.exit_code = 0;
// Filesystem abstraction: use in-memory map when opts.fs is set
auto fs_exists = [&](std::string_view p) -> bool {
if (opts.fs) return opts.fs->contains(std::string(p));
return file_exists(p);
};
auto fs_read = [&](std::string_view p) -> std::string {
if (opts.fs) {
auto it = opts.fs->find(std::string(p));
return it != opts.fs->end() ? it->second : std::string{};
}
return read_file(p);
};
auto fs_write = [&](std::string_view p, std::string_view c) -> bool {
if (opts.fs) { (*opts.fs)[std::string(p)] = std::string(c); return true; }
return write_file(p, c);
};
auto fs_delete = [&](std::string_view p) -> bool {
if (opts.fs) { opts.fs->erase(std::string(p)); return true; }
return delete_file(p);
};
auto files = parse_patch(patch_text, opts.strip_level, opts.reverse);
if (files.empty()) {
return result;
}
bool had_rejects = false;
for (const auto &pf : files) {
if (pf.target_path.empty()) continue;
if (!opts.quiet) {
result.out += "patching file " + pf.target_path + "\n";
}
// Load current file contents
FileContent fc;
bool file_existed = fs_exists(pf.target_path);
if (pf.is_creation && file_existed) {
// File exists but patch says it should be new — still try to apply
}
if (file_existed) {
fc = load_file_lines(fs_read(pf.target_path));
} else if (!pf.is_creation) {
// File doesn't exist and this isn't a creation patch
result.err += "can't find file to patch at input line 0\n";
if (!opts.force) {
result.exit_code = 1;
if (!opts.dry_run) {
had_rejects = true;
// Write all hunks as rejects
std::vector<bool> all_rejected(checked_cast<size_t>(std::ssize(pf.hunks)), true);
std::string rej_content = format_rejects(pf, all_rejected);
if (!rej_content.empty()) {
fs_write(pf.target_path + ".rej", rej_content);
}
}
continue;
}
}
// Try to match each hunk
std::vector<ptrdiff_t> hunk_positions(checked_cast<size_t>(std::ssize(pf.hunks)), -1);
std::vector<HunkFuzz> hunk_fuzz(checked_cast<size_t>(std::ssize(pf.hunks)));
std::vector<bool> rejected(checked_cast<size_t>(std::ssize(pf.hunks)), false);
ptrdiff_t cumulative_offset = 0;
ptrdiff_t last_frozen_line = 0; // 0-based, exclusive: lines before this are frozen
bool file_has_rejects = false;
for (ptrdiff_t h = 0; h < std::ssize(pf.hunks); ++h) {
const auto &hunk = pf.hunks[checked_cast<size_t>(h)];
auto pattern = get_old_pattern(hunk);
ptrdiff_t pos = locate_hunk(fc.lines, hunk, pattern,
last_frozen_line, cumulative_offset,
opts.fuzz);
if (pos >= 0) {
hunk_positions[checked_cast<size_t>(h)] = pos;
ptrdiff_t pat_len = std::ssize(pattern);
ptrdiff_t actual_offset = pos - (std::max(hunk.old_start, ptrdiff_t{1}) - 1);
auto ctx = get_hunk_context(hunk);
// Determine fuzz level used for this hunk
int fuzz_used = 0;
if (opts.fuzz > 0) {
for (int f = 0; f <= opts.fuzz; ++f) {
if (try_match(fc.lines, pos, pattern, f, ctx.prefix, ctx.suffix)) {
fuzz_used = f;
break;
}
}
}
// Record fuzz amounts for build_output trimming
hunk_fuzz[checked_cast<size_t>(h)] = {
std::min(static_cast<ptrdiff_t>(fuzz_used), ctx.prefix),
std::min(static_cast<ptrdiff_t>(fuzz_used), ctx.suffix)
};
auto &fz = hunk_fuzz[checked_cast<size_t>(h)];
if (actual_offset != cumulative_offset && !opts.quiet) {
if (fuzz_used > 0) {
result.out += std::format(
"Hunk #{} succeeded at {} with fuzz {} (offset {} lines).\n",
h + 1, pos + 1, fuzz_used, actual_offset - cumulative_offset);
} else {
result.out += std::format(
"Hunk #{} succeeded at {} (offset {} lines).\n",
h + 1, pos + 1, actual_offset - cumulative_offset);
}
} else if (fuzz_used > 0 && !opts.quiet) {
result.out += std::format(
"Hunk #{} succeeded at {} with fuzz {}.\n",
h + 1, pos + 1, fuzz_used);
}
// Update offset and frozen line (adjusted for fuzz)
cumulative_offset = actual_offset;
last_frozen_line = pos + pat_len - fz.suffix;
} else {
// Hunk failed
rejected[checked_cast<size_t>(h)] = true;
file_has_rejects = true;
if (!opts.quiet) {
if (opts.merge) {
result.err += std::format("Hunk #{} NOT MERGED at {}.\n",
h + 1, hunk.old_start);
} else {
result.err += std::format("Hunk #{} FAILED at {}.\n",
h + 1, hunk.old_start);
}
}
}
}
if (file_has_rejects) {
had_rejects = true;
result.exit_code = 1;
}
// Apply changes
if (!opts.dry_run) {
bool any_applied = false;
for (ptrdiff_t h = 0; h < std::ssize(pf.hunks); ++h) {
if (hunk_positions[checked_cast<size_t>(h)] >= 0) { any_applied = true; break; }
}
if (any_applied || pf.is_creation || (opts.merge && file_has_rejects)) {
std::string new_content;
if (opts.merge && file_has_rejects) {
new_content = build_merge_output(fc.lines, fc.has_trailing_newline,
pf, hunk_positions, hunk_fuzz,
opts.merge_style);
} else {
new_content = build_output(fc.lines, fc.has_trailing_newline,
pf, hunk_positions, hunk_fuzz);
}
// Restore \r\n line endings if the original file used them
if (fc.crlf) {
std::string crlf_content;
crlf_content.reserve(new_content.size() + new_content.size() / 40);
for (size_t k = 0; k < new_content.size(); ++k) {
if (new_content[k] == '\n' &&
(k == 0 || new_content[k - 1] != '\r')) {
crlf_content += '\r';
}
crlf_content += new_content[k];
}
new_content = std::move(crlf_content);
}
// Create parent directories if needed
if (!opts.fs) {
std::string dir = dirname(pf.target_path);
if (!dir.empty() && dir != "." && !is_directory(dir)) {
make_dirs(dir);
}
}
// Check if we should remove the file (-E flag)
if (opts.remove_empty && new_content.empty() && !pf.is_creation) {
if (file_existed) {
fs_delete(pf.target_path);
}
} else {
fs_write(pf.target_path, new_content);
}
}
// Write reject file if needed (and not in merge mode)
if (file_has_rejects && !opts.merge) {
std::string rej_content = format_rejects(pf, rejected);
if (!rej_content.empty()) {
fs_write(pf.target_path + ".rej", rej_content);
}
if (!opts.quiet) {
ptrdiff_t rej_count = 0;
for (bool r : rejected) if (r) ++rej_count;
result.err += std::format(
"{} out of {} {} FAILED -- saving rejects to file {}.rej\n",
rej_count, std::ssize(pf.hunks),
std::ssize(pf.hunks) == 1 ? "hunk" : "hunks",
pf.target_path);
}
}
}
}
if (had_rejects) {
result.exit_code = 1;
}
return result;
}
// === src/cmd_stack.cpp ===
// This is free and unencumbered software released into the public domain.
#include <cstdlib>
static void write_applied_patches(QuiltState &q) {
std::string path = path_join(q.work_dir, q.pc_dir, "applied-patches");
write_applied(path, q.applied);
}
// Parse affected files from a unified diff, stripping path components
// to match what `patch -pN` would do.
static std::vector<std::string> parse_patch_files(std::string_view content, int strip = 1) {
std::vector<std::string> files;
auto lines = split_lines(content);
for (const auto &line : lines) {
if (!line.starts_with("+++ ")) continue;
std::string_view rest = std::string_view(line).substr(4);
// Skip /dev/null
if (rest.starts_with("/dev/null")) continue;
// Strip trailing tab and timestamp (e.g., "\t2024-01-01 ...")
ptrdiff_t tab = str_find(rest, '\t');
if (tab >= 0) {
rest = rest.substr(0, checked_cast<size_t>(tab));
}
std::string f = trim(rest);
// Strip N leading path components (like patch -pN)
for (int i = 0; i < strip && !f.empty(); ++i) {
ptrdiff_t slash = str_find(std::string_view(f), '/');
if (slash >= 0) {
f = f.substr(checked_cast<size_t>(slash) + 1);
}
}
if (!f.empty()) {
files.push_back(std::move(f));
}
}
return files;
}
int cmd_series(QuiltState &q, int argc, char **argv) {
bool verbose = false;
// color: 0=never, 1=auto, 2=always
int color_mode = 0;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-v") {
verbose = true;
} else if (arg == "--color") {
color_mode = 1; // auto
} else if (arg.starts_with("--color=")) {
auto val = arg.substr(8);
if (val == "always") color_mode = 2;
else if (val == "auto") color_mode = 1;
else if (val == "never") color_mode = 0;
else {
err("Invalid --color value: "); err_line(val);
return 1;
}
} else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
}
bool use_color = (color_mode == 2) || (color_mode == 1 && stdout_is_tty());
if (q.series.empty()) {
if (q.series_file_exists) {
// Empty series file: nothing to print, success
return 0;
} else {
err_line("No series file found");
return 1;
}
}
for (const auto &patch : q.series) {
if (verbose) {
if (!q.applied.empty() && patch == q.applied.back()) {
out("= ");
} else if (q.is_applied(patch)) {
out("+ ");
} else {
out(" ");
}
}
std::string name = format_patch(q, patch);
if (use_color) {
if (!q.applied.empty() && patch == q.applied.back()) {
out("\033[33m"); // yellow for top
} else if (q.is_applied(patch)) {
out("\033[32m"); // green for applied
} else {
out("\033[00m"); // default for unapplied
}
out(name);
out_line("\033[00m");
} else {
out_line(name);
}
}
return 0;
}
int cmd_applied(QuiltState &q, int argc, char **argv) {
std::string_view target;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
target = strip_patches_prefix(q, arg);
}
if (!target.empty()) {
// Print all applied patches up to and including target
auto idx = q.find_in_series(target);
if (!idx.has_value()) {
err("Patch "); err(format_patch(q, target)); err_line(" is not in series");
return 1;
}
if (!q.is_applied(target)) {
err("Patch "); err(format_patch(q, target)); err_line(" is not applied");
return 1;
}
for (const auto &a : q.applied) {
out_line(format_patch(q, a));
if (a == target) break;
}
return 0;
}
if (q.series.empty()) {
if (q.series_file_exists) {
err_line("No patches in series");
} else {
err_line("No series file found");
}
return 1;
}
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
for (const auto &a : q.applied) {
out_line(format_patch(q, a));
}
return 0;
}
int cmd_unapplied(QuiltState &q, int argc, char **argv) {
std::string_view target;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
target = strip_patches_prefix(q, arg);
}
if (q.series.empty()) {
if (q.series_file_exists) {
err_line("No patches in series");
} else {
err_line("No series file found");
}
return 1;
}
ptrdiff_t start_idx;
if (!target.empty()) {
auto idx = q.find_in_series(target);
if (!idx.has_value()) {
err("Patch "); err(format_patch(q, target)); err_line(" is not in series");
return 1;
}
start_idx = idx.value() + 1;
} else {
ptrdiff_t top = q.top_index();
start_idx = top + 1;
}
if (start_idx >= std::ssize(q.series)) {
// With an explicit target patch, having no patches after it is not
// an error — just print nothing.
if (!target.empty()) {
return 0;
}
std::string_view top_name = q.applied.empty() ? std::string_view("??") : std::string_view(q.applied.back());
err("File series fully applied, ends at patch "); err_line(format_patch(q, top_name));
return 1;
}
for (ptrdiff_t i = start_idx; i < std::ssize(q.series); ++i) {
out_line(format_patch(q, q.series[checked_cast<size_t>(i)]));
}
return 0;
}
int cmd_top(QuiltState &q, int argc, char **argv) {
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
}
if (q.series.empty()) {
if (q.series_file_exists) {
err_line("No patches in series");
return 2;
} else {
err_line("No series file found");
return 1;
}
}
if (q.applied.empty()) {
err_line("No patches applied");
return 2;
}
out_line(format_patch(q, q.applied.back()));
return 0;
}
int cmd_next(QuiltState &q, int argc, char **argv) {
std::string_view target;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
target = strip_patches_prefix(q, arg);
}
if (q.series.empty()) {
if (q.series_file_exists) {
err_line("No patches in series");
return 2;
} else {
err_line("No series file found");
return 1;
}
}
ptrdiff_t after_idx;
if (!target.empty()) {
auto idx = q.find_in_series(target);
if (!idx.has_value()) {
err("Patch "); err(format_patch(q, target)); err_line(" is not in series");
return 2;
}
// Original quilt: if the named patch is applied, error
if (q.is_applied(target)) {
err("Patch "); err(format_patch(q, target)); err_line(" is currently applied");
return 2;
}
// If unapplied, return the patch itself (it's the "next" to be pushed)
out_line(format_patch(q, target));
return 0;
} else {
ptrdiff_t top = q.top_index();
after_idx = top + 1;
}
if (after_idx >= std::ssize(q.series)) {
std::string_view top_name = q.applied.empty() ? std::string_view("??") : std::string_view(q.applied.back());
err("File series fully applied, ends at patch "); err_line(format_patch(q, top_name));
return 2;
}
out_line(format_patch(q, q.series[checked_cast<size_t>(after_idx)]));
return 0;
}
int cmd_previous(QuiltState &q, int argc, char **argv) {
std::string_view target;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
target = strip_patches_prefix(q, arg);
}
if (!target.empty()) {
auto idx = q.find_in_series(target);
if (!idx.has_value()) {
err("Patch "); err(format_patch(q, target)); err_line(" is not in series");
return 2;
}
if (idx.value() == 0) {
return 2;
}
out_line(format_patch(q, q.series[checked_cast<size_t>(idx.value() - 1)]));
return 0;
}
if (q.series.empty()) {
if (q.series_file_exists) {
err_line("No patches in series");
return 2;
} else {
err_line("No series file found");
return 1;
}
}
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
if (std::ssize(q.applied) == 1) {
return 2;
}
out_line(format_patch(q, q.applied[checked_cast<size_t>(std::ssize(q.applied) - 2)]));
return 0;
}
int cmd_push(QuiltState &q, int argc, char **argv) {
bool push_all = false;
bool force = false;
bool quiet = false;
bool verbose = false;
int fuzz = -1;
bool merge = false;
std::string merge_style;
bool leave_rejects = false;
bool do_refresh = false;
int push_count = -1;
std::string_view target;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-a") { push_all = true; }
else if (arg == "-f") { force = true; }
else if (arg == "-q" || arg == "--quiet") { quiet = true; }
else if (arg == "-v" || arg == "--verbose") { verbose = true; }
else if (arg.starts_with("--fuzz=")) { fuzz = checked_cast<int>(parse_int(arg.substr(7))); }
else if (arg == "-m" || arg == "--merge") { merge = true; }
else if (arg.starts_with("--merge=")) { merge = true; merge_style = std::string(arg.substr(8)); }
else if (arg == "--leave-rejects") { leave_rejects = true; }
else if (arg == "--refresh") { do_refresh = true; }
else if (arg == "--color" || arg.starts_with("--color=")) {
if (arg.starts_with("--color=")) {
auto val = arg.substr(8);
if (val != "always" && val != "auto" && val != "never") {
err("Invalid --color value: "); err_line(val);
return 1;
}
}
}
else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
else {
// Try as number first
int val = 0;
auto [ptr, ec] = std::from_chars(arg.data(), arg.data() + arg.size(), val);
if (ec == std::errc{} && ptr == arg.data() + arg.size() && val > 0) {
push_count = val;
} else {
target = strip_patches_prefix(q, arg);
}
}
}
// Refuse to push if top patch needs refresh (was force-applied)
if (!q.applied.empty()) {
std::string nr = path_join(pc_patch_dir(q, q.applied.back()), ".needs_refresh");
if (file_exists(nr)) {
err_line("The topmost patch " + patch_path_display(q, q.applied.back()) +
" needs to be refreshed first.");
return 1;
}
}
ptrdiff_t top = q.top_index();
ptrdiff_t start_idx = top + 1;
if (q.series.empty()) {
if (q.series_file_exists) {
err_line("No patches in series");
} else {
err_line("No series file found");
}
return 2;
}
if (start_idx >= std::ssize(q.series)) {
err_line("File series fully applied, ends at patch " +
patch_path_display(q, q.applied.back()));
return 2;
}
ptrdiff_t end_idx; // inclusive
if (push_all) {
end_idx = std::ssize(q.series) - 1;
} else if (!target.empty()) {
auto idx = q.find_in_series(target);
if (!idx.has_value()) {
err("Patch "); err(format_patch(q, target)); err_line(" is not in series");
return 1;
}
end_idx = idx.value();
if (end_idx < start_idx) {
err("Patch "); err(format_patch(q, target)); err_line(" is currently applied");
return 2;
}
} else if (push_count > 0) {
end_idx = start_idx + push_count - 1;
if (end_idx >= std::ssize(q.series)) {
end_idx = std::ssize(q.series) - 1;
}
} else {
end_idx = start_idx;
}
if (!ensure_pc_dir(q)) return 1;
// Read QUILT_PATCH_OPTS
auto extra_patch_opts = shell_split(get_env("QUILT_PATCH_OPTS"));
std::string last_applied;
for (ptrdiff_t i = start_idx; i <= end_idx; ++i) {
const std::string &name = q.series[checked_cast<size_t>(i)];
std::string display = patch_path_display(q, name);
if (i > start_idx) {
out_line("");
}
out_line("Applying patch " + display);
// Read patch file
std::string patch_path = path_join(q.work_dir, q.patches_dir, name);
std::string patch_content = read_file(patch_path);
if (patch_content.empty() && !file_exists(patch_path)) {
err_line("Patch " + display + " does not exist");
return 1;
}
// Parse affected files and back them up
int strip_level = q.get_strip_level(name);
auto affected = parse_patch_files(patch_content, strip_level);
std::string pc_dir = pc_patch_dir(q, name);
if (!is_directory(pc_dir)) {
make_dirs(pc_dir);
}
for (const auto &file : affected) {
backup_file(q, name, file);
}
// Apply the patch using built-in patch engine
PatchOptions patch_opts;
patch_opts.strip_level = strip_level;
patch_opts.remove_empty = true;
patch_opts.force = force;
if (q.patch_reversed.contains(name)) patch_opts.reverse = true;
if (fuzz >= 0) patch_opts.fuzz = fuzz;
if (merge) {
patch_opts.merge = true;
patch_opts.merge_style = merge_style;
}
// Parse QUILT_PATCH_OPTS for additional options
for (const auto &opt : extra_patch_opts) {
std::string_view o = opt;
if (o == "-R") patch_opts.reverse = true;
else if (o == "-f" || o == "--force") patch_opts.force = true;
else if (o == "-s") patch_opts.quiet = true;
else if (o == "-E") patch_opts.remove_empty = true;
else if (o.starts_with("--fuzz=")) {
patch_opts.fuzz = checked_cast<int>(parse_int(o.substr(7)));
}
}
// Print verbose file list ourselves instead of relying on
// patch --verbose, which is not available on busybox.
if (verbose && !quiet) {
for (const auto &file : affected) {
out_line("patching file " + file);
}
}
// Suppress builtin_patch's own "patching file" messages when we do verbose ourselves
if (verbose) patch_opts.quiet = true;
PatchResult result = builtin_patch(patch_content, patch_opts);
if (!quiet && !verbose && !result.out.empty()) {
out(result.out);
}
if (result.exit_code != 0) {
if (!result.err.empty()) {
err(result.err);
}
if (force) {
// Force-applied: record as applied but mark as needing refresh
q.applied.push_back(name);
write_applied_patches(q);
write_file(path_join(pc_dir, ".timestamp"), "");
write_file(path_join(pc_dir, ".needs_refresh"), "");
out_line("Applied patch " + display + " (forced; needs refresh)");
return 1;
} else {
// Not forced: restore files from backups and clean up
for (const auto &file : affected) {
restore_file(q, name, file);
}
err_line("Patch " + display + " does not apply (enforce with -f)");
if (!leave_rejects) {
for (const auto &file : affected) {
std::string rej = path_join(q.work_dir, file + ".rej");
if (file_exists(rej)) {
delete_file(rej);
}
}
}
delete_dir_recursive(pc_dir);
return 1;
}
}
// Record as applied
q.applied.push_back(name);
write_applied_patches(q);
// Create .timestamp
write_file(path_join(pc_dir, ".timestamp"), "");
if (do_refresh) {
char arg0[] = "refresh";
char *refresh_argv[] = {arg0, nullptr};
int rr = cmd_refresh(q, 1, refresh_argv);
if (rr != 0) return rr;
}
last_applied = name;
}
if (!last_applied.empty()) {
if (!quiet) out_line("");
out_line("Now at patch " + patch_path_display(q, last_applied));
}
return 0;
}
int cmd_pop(QuiltState &q, int argc, char **argv) {
bool pop_all = false;
bool force = false;
bool quiet = false;
[[maybe_unused]] bool verbose = false; // accepted for compat, pop is verbose by default
bool auto_refresh = false;
int pop_count = -1;
std::string_view target;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-a") { pop_all = true; }
else if (arg == "-f") { force = true; }
else if (arg == "-q" || arg == "--quiet") { quiet = true; }
else if (arg == "-v" || arg == "--verbose") { verbose = true; }
else if (arg == "-R") { /* accepted for compat, always verified now */ }
else if (arg == "--refresh") { auto_refresh = true; }
else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
else {
// Try as number first
int val = 0;
auto [ptr, ec] = std::from_chars(arg.data(), arg.data() + arg.size(), val);
if (ec == std::errc{} && ptr == arg.data() + arg.size() && val > 0) {
pop_count = val;
} else {
target = strip_patches_prefix(q, arg);
}
}
}
if (q.applied.empty()) {
if (!q.series_file_exists) {
err_line("No series file found");
return 1;
}
err_line("No patch removed");
return 2;
}
ptrdiff_t stop_idx; // index in applied to stop BEFORE (exclusive); pop down to this
if (pop_all) {
stop_idx = 0;
} else if (!target.empty()) {
// Find target in applied list
ptrdiff_t found_idx = -1;
for (ptrdiff_t i = 0; i < std::ssize(q.applied); ++i) {
if (q.applied[checked_cast<size_t>(i)] == target) {
found_idx = i;
break;
}
}
if (found_idx < 0) {
err("Patch "); err(format_patch(q, target)); err_line(" is not applied");
return 1;
}
// Pop down to (but not including) the target patch
stop_idx = found_idx + 1;
if (stop_idx >= std::ssize(q.applied)) {
err_line("No patch removed");
return 2;
}
} else if (pop_count > 0) {
stop_idx = std::ssize(q.applied) - pop_count;
if (stop_idx < 0) stop_idx = 0;
} else {
// Pop just the top patch
stop_idx = std::ssize(q.applied) - 1;
}
// Pop from the top down to stop_idx
bool first_pop = true;
while (std::ssize(q.applied) > stop_idx) {
const std::string &name = q.applied.back();
std::string display = patch_path_display(q, name);
// Auto-refresh before popping if requested
if (auto_refresh) {
auto extra = shell_split(get_env("QUILT_REFRESH_ARGS"));
std::vector<std::string> r_storage;
r_storage.push_back("refresh");
for (auto &e : extra) r_storage.push_back(e);
std::vector<char *> r_argv;
for (auto &s : r_storage) r_argv.push_back(s.data());
int rr = cmd_refresh(q, checked_cast<int>(std::ssize(r_argv)), r_argv.data());
if (rr != 0) {
err_line("Refresh of patch " + display + " failed, aborting pop");
return 1;
}
}
// Check if patch needs refresh (force-applied) and -f not given
std::string pc_dir = pc_patch_dir(q, name);
std::string nr = path_join(pc_dir, ".needs_refresh");
if (file_exists(nr) && !force && !auto_refresh) {
err_line("Patch " + display + " needs to be refreshed first.");
return 1;
}
// Check if patch removes cleanly (detects dirty/unrefreshed changes)
if (!force) {
std::string patch_path = path_join(q.work_dir, q.patches_dir, name);
std::string patch_content = read_file(patch_path);
if (!patch_content.empty()) {
int strip_level = q.get_strip_level(name);
PatchOptions verify_opts;
verify_opts.strip_level = strip_level;
verify_opts.reverse = true;
verify_opts.dry_run = true;
verify_opts.force = true;
verify_opts.quiet = true;
PatchResult vr = builtin_patch(patch_content, verify_opts);
if (vr.exit_code != 0) {
err_line("Patch " + display +
" does not remove cleanly (refresh it or enforce with -f)");
err_line("Hint: `quilt diff -z' will show the pending changes.");
return 1;
}
}
}
if (!first_pop) {
out_line("");
}
out_line("Removing patch " + display);
first_pop = false;
// Restore backed-up files
auto files = files_in_patch(q, name);
for (const auto &file : files) {
restore_file(q, name, file);
if (!quiet) {
// Show what happened to each file: "Removing" if the file
// was deleted (created by the patch), "Restoring" otherwise.
if (!file_exists(path_join(q.work_dir, file))) {
out_line("Removing " + file);
} else {
out_line("Restoring " + file);
}
}
}
// Remove the backup directory
delete_dir_recursive(pc_dir);
// Remove from applied list
q.applied.pop_back();
write_applied_patches(q);
}
if (!quiet) out_line("");
if (q.applied.empty()) {
out_line("No patches applied");
} else {
out_line("Now at patch " + patch_path_display(q, q.applied.back()));
}
return 0;
}
// === src/cmd_annotate.cpp ===
// This is free and unencumbered software released into the public domain.
#include <optional>
namespace {
struct AnnotateOptions {
std::string patch;
std::string file;
};
static bool path_has_content(std::string_view path)
{
return file_exists(path) && !read_file(path).empty();
}
static std::vector<std::string> read_lines(std::string_view path)
{
if (!path_has_content(path)) {
return {};
}
return split_lines(read_file(path));
}
static std::string next_patch_for_file(const QuiltState &q,
std::string_view patch,
std::string_view file)
{
bool after_target = false;
for (const auto &applied : q.applied) {
if (after_target) {
auto tracked = files_in_patch(q, applied);
if (std::ranges::find(tracked, file) != tracked.end()) {
return applied;
}
}
if (applied == patch) {
after_target = true;
}
}
return "";
}
static std::vector<std::string> reannotate_lines(std::span<const std::string> old_lines,
std::span<const std::string> old_annotations,
std::span<const std::string> new_lines,
std::string_view annotation)
{
const ptrdiff_t m = std::ssize(old_lines);
const ptrdiff_t n = std::ssize(new_lines);
std::vector<std::vector<int>> dp(checked_cast<size_t>(m + 1), std::vector<int>(checked_cast<size_t>(n + 1), 0));
for (ptrdiff_t i = m; i-- > 0;) {
for (ptrdiff_t j = n; j-- > 0;) {
if (old_lines[checked_cast<size_t>(i)] == new_lines[checked_cast<size_t>(j)]) {
dp[checked_cast<size_t>(i)][checked_cast<size_t>(j)] = dp[checked_cast<size_t>(i + 1)][checked_cast<size_t>(j + 1)] + 1;
} else {
dp[checked_cast<size_t>(i)][checked_cast<size_t>(j)] = std::max(dp[checked_cast<size_t>(i + 1)][checked_cast<size_t>(j)], dp[checked_cast<size_t>(i)][checked_cast<size_t>(j + 1)]);
}
}
}
std::vector<std::string> result;
result.reserve(checked_cast<size_t>(n));
ptrdiff_t i = 0;
ptrdiff_t j = 0;
while (i < m || j < n) {
if (i < m && j < n && old_lines[checked_cast<size_t>(i)] == new_lines[checked_cast<size_t>(j)]) {
result.push_back(i < std::ssize(old_annotations) ? old_annotations[checked_cast<size_t>(i)] : "");
++i;
++j;
} else if (j < n && (i == m || dp[checked_cast<size_t>(i)][checked_cast<size_t>(j + 1)] >= dp[checked_cast<size_t>(i + 1)][checked_cast<size_t>(j)])) {
result.emplace_back(annotation);
++j;
} else {
++i;
}
}
return result;
}
std::optional<AnnotateOptions> parse_options(const QuiltState &q, int argc, char **argv)
{
AnnotateOptions opts;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-P" && i + 1 < argc) {
opts.patch = strip_patches_prefix(q, argv[i + 1]);
++i;
continue;
}
if (!arg.empty() && arg[0] == '-') {
return std::nullopt;
}
if (!opts.file.empty()) {
return std::nullopt;
}
opts.file = subdir_path(q, arg);
}
if (opts.file.empty()) {
return std::nullopt;
}
return opts;
}
int no_applied_patches_error(const QuiltState &q)
{
if (!q.series_file_exists) {
err_line("No series file found");
} else if (q.series.empty()) {
err_line("No patches in series");
} else {
err_line("No patches applied");
}
return 1;
}
} // namespace
int cmd_annotate(QuiltState &q, int argc, char **argv)
{
auto opts = parse_options(q, argc, argv);
if (!opts.has_value()) {
err_line("Usage: quilt annotate [-P patch] file");
return 1;
}
if (q.applied.empty()) {
return no_applied_patches_error(q);
}
std::string stop_patch = opts->patch.empty() ? q.applied.back() : opts->patch;
if (!q.find_in_series(stop_patch).has_value()) {
err_line("Patch " + stop_patch + " is not in series");
return 1;
}
if (!q.is_applied(stop_patch)) {
err_line("Patch " + stop_patch + " is not applied");
return 1;
}
std::vector<std::string> patches;
std::vector<std::string> files;
std::string next_patch;
for (const auto &patch : q.applied) {
std::string old_file = path_join(pc_patch_dir(q, patch), opts->file);
if (file_exists(old_file)) {
patches.push_back(patch);
files.push_back(old_file);
}
if (patch == stop_patch) {
next_patch = next_patch_for_file(q, stop_patch, opts->file);
break;
}
}
if (next_patch.empty()) {
files.push_back(path_join(q.work_dir, opts->file));
} else {
files.push_back(path_join(pc_patch_dir(q, next_patch), opts->file));
}
if (patches.empty()) {
std::string target = files.back();
if (!file_exists(target)) {
err_line("File " + opts->file + " does not exist");
return 1;
}
for (const auto &line : read_lines(target)) {
out("\t" + line + "\n");
}
return 0;
}
std::vector<std::string> annotations(checked_cast<size_t>(std::ssize(read_lines(files.front()))), "");
for (ptrdiff_t i = 0; i < std::ssize(patches); ++i) {
annotations = reannotate_lines(read_lines(files[checked_cast<size_t>(i)]), annotations,
read_lines(files[checked_cast<size_t>(i + 1)]),
std::to_string(i + 1));
}
auto final_lines = read_lines(files.back());
for (ptrdiff_t i = 0; i < std::ssize(annotations); ++i) {
std::string line = i < std::ssize(final_lines) ? final_lines[checked_cast<size_t>(i)] : "";
out(annotations[checked_cast<size_t>(i)] + "\t" + line + "\n");
}
out("\n");
for (ptrdiff_t i = 0; i < std::ssize(patches); ++i) {
out(std::to_string(i + 1) + "\t" + format_patch(q, patches[checked_cast<size_t>(i)]) + "\n");
}
return 0;
}
// === src/cmd_patch.cpp ===
// This is free and unencumbered software released into the public domain.
#include <cstring>
#include <cstdlib>
#include <set>
static std::string read_patch_header(std::string_view patch_path) {
std::string content = read_file(patch_path);
if (content.empty()) return "";
std::string header;
auto lines = split_lines(content);
for (const auto &line : lines) {
if (line.starts_with("Index:") ||
line.starts_with("---") ||
line.starts_with("diff ")) {
break;
}
header += line;
header += '\n';
}
return header;
}
int cmd_init(QuiltState &q, int argc, char **) {
if (argc != 1) {
err_line("Usage: quilt init");
return 1;
}
q.work_dir = get_cwd();
q.pc_dir = ".pc";
q.patches_dir = "patches";
std::string env_pc = get_env("QUILT_PC");
if (!env_pc.empty()) {
q.pc_dir = env_pc;
}
std::string env_patches = get_env("QUILT_PATCHES");
if (!env_patches.empty()) {
q.patches_dir = env_patches;
}
std::string series_name = get_env("QUILT_SERIES");
if (series_name.empty()) {
series_name = "series";
}
q.series_file = path_join(q.patches_dir, series_name);
if (!ensure_pc_dir(q)) {
return 1;
}
std::string patches_abs = path_join(q.work_dir, q.patches_dir);
if (!is_directory(patches_abs)) {
if (!make_dirs(patches_abs)) {
err_line("Failed to create " + patches_abs);
return 1;
}
}
std::string series_abs = path_join(q.work_dir, q.series_file);
if (!file_exists(series_abs)) {
if (!write_series(series_abs, {}, {}, {})) {
err_line("Failed to write series file.");
return 1;
}
}
std::string applied_abs = path_join(q.work_dir, q.pc_dir, "applied-patches");
if (!file_exists(applied_abs)) {
if (!write_applied(applied_abs, {})) {
err_line("Failed to write applied-patches.");
return 1;
}
}
out_line("The quilt meta-data is now initialized.");
return 0;
}
int cmd_new(QuiltState &q, int argc, char **argv) {
// Parse options
std::string patch_name;
std::string p_value;
int i = 1; // skip argv[0] which is "new"
while (i < argc) {
std::string_view arg = argv[i];
if (arg == "-p" && i + 1 < argc) {
p_value = argv[i + 1];
i += 2;
continue;
}
if (arg.starts_with("-p") && std::ssize(arg) > 2) {
p_value = std::string(arg.substr(2));
i += 1;
continue;
}
// First non-option argument is the patch name
if (arg[0] != '-') {
patch_name = std::string(arg);
i += 1;
break;
}
err("Unrecognized option: "); err_line(arg);
return 1;
}
if (patch_name.empty()) {
err_line("Usage: quilt new [-p n] patchname");
return 1;
}
// Verify patch doesn't already exist in series
if (q.find_in_series(patch_name).has_value()) {
err("Patch "); err(patch_name); err_line(" already exists in series.");
return 1;
}
// Ensure .pc/ directory exists
if (!ensure_pc_dir(q)) return 1;
// Ensure patches/ directory exists
std::string patches_abs = path_join(q.work_dir, q.patches_dir);
if (!is_directory(patches_abs)) {
if (!make_dirs(patches_abs)) {
err_line("Failed to create " + patches_abs);
return 1;
}
}
// Insert patch name into series (after current top, or at beginning)
ptrdiff_t top_idx = q.top_index();
if (!q.applied.empty() && top_idx < 0) {
err_line("The series file no longer matches the applied patches. Please run 'quilt pop -a'.");
return 1;
}
if (top_idx < 0) {
// No applied patches — insert at beginning
q.series.insert(q.series.begin(), patch_name);
} else {
// Insert after the current top
q.series.insert(q.series.begin() + top_idx + 1, patch_name);
}
// Validate strip level
if (!p_value.empty() && p_value != "0" && p_value != "1") {
err_line("Cannot create patches with -p" + p_value +
", please specify -p0 or -p1 instead");
return 1;
}
// Store strip level
if (!p_value.empty() && p_value != "1") {
q.patch_strip_level[patch_name] = checked_cast<int>(parse_int(p_value));
}
// Write series file
std::string series_abs = path_join(q.work_dir, q.series_file);
if (!write_series(series_abs, q.series, q.patch_strip_level, q.patch_reversed)) {
err_line("Failed to write series file.");
return 1;
}
// Add to applied list and write applied-patches
q.applied.push_back(patch_name);
std::string applied_abs = path_join(q.work_dir, q.pc_dir, "applied-patches");
if (!write_applied(applied_abs, q.applied)) {
err_line("Failed to write applied-patches.");
return 1;
}
// Create .pc/<patchname>/ directory
std::string pc_dir = pc_patch_dir(q, patch_name);
if (!is_directory(pc_dir)) {
if (!make_dirs(pc_dir)) {
err_line("Failed to create " + pc_dir);
return 1;
}
}
out_line("Patch " + patch_path_display(q, patch_name) + " is now on top");
return 0;
}
int cmd_add(QuiltState &q, int argc, char **argv) {
if (q.applied.empty()) {
if (!q.series_file_exists) {
err_line("No series file found");
return 1;
}
err_line("No patches applied");
return 1;
}
// Parse options
std::string_view patch = q.applied.back();
std::vector<std::string> files;
int i = 1;
while (i < argc) {
std::string_view arg = argv[i];
if (arg == "-P" && i + 1 < argc) {
patch = strip_patches_prefix(q, argv[i + 1]);
i += 2;
continue;
}
if (arg[0] != '-') {
files.push_back(subdir_path(q, arg));
} else {
err("Unrecognized option: "); err_line(arg);
return 1;
}
i += 1;
}
if (files.empty()) {
err_line("Usage: quilt add [-P patch] file ...");
return 1;
}
if (!q.is_applied(patch)) {
err("Patch "); err(format_patch(q, patch)); err_line(" is not applied");
return 1;
}
for (const auto &file : files) {
// Check if file is already tracked by this patch
std::string backup_path = path_join(pc_patch_dir(q, patch), file);
if (file_exists(backup_path)) {
err("File "); err(file); err(" is already in patch ");
err_line(patch_path_display(q, patch));
return 2;
}
// Check if file is modified by any patch applied after this one
bool found_patch = false;
for (const auto &ap : q.applied) {
if (!found_patch) {
if (ap == patch) found_patch = true;
continue;
}
std::string later_backup = path_join(pc_patch_dir(q, ap), file);
if (file_exists(later_backup)) {
err("File "); err(file); err(" modified by patch ");
err_line(patch_path_display(q, ap));
return 1;
}
}
// Backup the file
if (!backup_file(q, patch, file)) {
err("Failed to back up "); err_line(file);
return 1;
}
out("File "); out(file); out(" added to patch ");
out_line(patch_path_display(q, patch));
}
return 0;
}
int cmd_remove(QuiltState &q, int argc, char **argv) {
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
// Parse options
std::string_view patch = q.applied.back();
std::vector<std::string> files;
int i = 1;
while (i < argc) {
std::string_view arg = argv[i];
if (arg == "-P" && i + 1 < argc) {
patch = strip_patches_prefix(q, argv[i + 1]);
i += 2;
continue;
}
if (arg[0] != '-') {
files.push_back(subdir_path(q, arg));
} else {
err("Unrecognized option: "); err_line(arg);
return 1;
}
i += 1;
}
if (files.empty()) {
err_line("Usage: quilt remove [-P patch] file ...");
return 1;
}
if (!q.is_applied(patch)) {
err("Patch "); err(format_patch(q, patch)); err_line(" is not applied");
return 1;
}
for (const auto &file : files) {
// Check if file is tracked by this patch
std::string backup_path = path_join(pc_patch_dir(q, patch), file);
if (!file_exists(backup_path)) {
err("File "); err(file); err(" is not in patch ");
err_line(patch_path_display(q, patch));
return 1;
}
// Restore file from backup
if (!restore_file(q, patch, file)) {
err("Failed to restore "); err_line(file);
return 1;
}
// Remove backup file
delete_file(backup_path);
out("File "); out(file); out(" removed from patch ");
out_line(patch_path_display(q, patch));
}
return 0;
}
int cmd_edit(QuiltState &q, int argc, char **argv) {
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
std::vector<std::string> files;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
files.push_back(subdir_path(q, arg));
}
if (files.empty()) {
err_line("Usage: quilt edit file ...");
return 1;
}
std::string_view patch = q.applied.back();
// Add each file to the top patch if not already tracked
for (const auto &file : files) {
std::string backup_path = path_join(pc_patch_dir(q, patch), file);
if (!file_exists(backup_path)) {
if (!backup_file(q, patch, file)) {
err("Failed to back up "); err_line(file);
return 1;
}
out("File "); out(file); out(" added to patch ");
out_line(patch_path_display(q, patch));
}
}
// Get editor from environment
std::string editor = get_env("EDITOR");
if (editor.empty()) {
editor = "vi";
}
// Launch editor with all files as arguments
std::vector<std::string> cmd_argv;
cmd_argv.push_back(editor);
for (const auto &file : files) {
cmd_argv.push_back(path_join(q.work_dir, file));
}
return run_cmd_tty(cmd_argv);
}
// Convert unified diff output to context diff format. This allows
// quilt to produce -c/-C output even when the external diff command
// (e.g. busybox) only supports unified format.
static std::string unified_to_context(std::string_view unified)
{
auto lines = split_lines(unified);
std::string result;
ptrdiff_t n = std::ssize(lines);
ptrdiff_t i = 0;
// File headers: unified --- becomes context ***, unified +++ becomes context ---
while (i < n && !lines[checked_cast<size_t>(i)].starts_with("--- ")) ++i;
if (i < n) { result += "*** " + lines[checked_cast<size_t>(i)].substr(4) + "\n"; ++i; }
if (i < n && lines[checked_cast<size_t>(i)].starts_with("+++ ")) {
result += "--- " + lines[checked_cast<size_t>(i)].substr(4) + "\n"; ++i;
}
while (i < n) {
if (!lines[checked_cast<size_t>(i)].starts_with("@@ ")) { ++i; continue; }
// Parse @@ -os[,oc] +ns[,nc] @@
int os = 0, oc = 1, ns = 0, nc = 1;
{
std::string_view hdr = lines[checked_cast<size_t>(i)];
ptrdiff_t at1 = str_find(hdr, '-', 3);
if (at1 >= 0) {
ptrdiff_t p = at1 + 1;
ptrdiff_t c = str_find(hdr, ',', p);
ptrdiff_t pp = str_find(hdr, '+', p);
if (pp >= 0) {
if (c >= 0 && c < pp) {
os = checked_cast<int>(parse_int(hdr.substr(checked_cast<size_t>(p), checked_cast<size_t>(c - p))));
oc = checked_cast<int>(parse_int(hdr.substr(checked_cast<size_t>(c + 1), checked_cast<size_t>(pp - c - 2))));
} else {
os = checked_cast<int>(parse_int(hdr.substr(checked_cast<size_t>(p), checked_cast<size_t>(pp - p - 1))));
}
p = pp + 1;
c = str_find(hdr, ',', p);
ptrdiff_t end = str_find(hdr, ' ', p);
if (end < 0) end = std::ssize(hdr);
if (c >= 0 && c < end) {
ns = checked_cast<int>(parse_int(hdr.substr(checked_cast<size_t>(p), checked_cast<size_t>(c - p))));
nc = checked_cast<int>(parse_int(hdr.substr(checked_cast<size_t>(c + 1), checked_cast<size_t>(end - c - 1))));
} else {
ns = checked_cast<int>(parse_int(hdr.substr(checked_cast<size_t>(p), checked_cast<size_t>(end - p))));
}
}
}
}
++i;
// Collect unified hunk body lines with their types
struct UL { char type; std::string text; };
std::vector<UL> body;
while (i < n && !lines[checked_cast<size_t>(i)].starts_with("@@ ")) {
std::string_view ln = lines[checked_cast<size_t>(i)];
body.push_back({ln.empty() ? ' ' : ln[0],
ln.empty() ? std::string{} : std::string(ln.substr(1))});
++i;
}
// Build old-side and new-side lines with context-diff prefixes.
// Adjacent -/+ runs form "change" blocks and get '!' prefix.
std::vector<std::pair<char, std::string>> old_side, new_side;
for (ptrdiff_t k = 0; k < std::ssize(body); ) {
if (body[checked_cast<size_t>(k)].type == ' ') {
old_side.push_back({' ', body[checked_cast<size_t>(k)].text});
new_side.push_back({' ', body[checked_cast<size_t>(k)].text});
++k;
} else if (body[checked_cast<size_t>(k)].type == '-') {
ptrdiff_t ds = k;
while (k < std::ssize(body) && body[checked_cast<size_t>(k)].type == '-') ++k;
ptrdiff_t as = k;
while (k < std::ssize(body) && body[checked_cast<size_t>(k)].type == '+') ++k;
bool change = (as > ds && k > as);
for (ptrdiff_t m = ds; m < as; ++m)
old_side.push_back({change ? '!' : '-', body[checked_cast<size_t>(m)].text});
for (ptrdiff_t m = as; m < k; ++m)
new_side.push_back({change ? '!' : '+', body[checked_cast<size_t>(m)].text});
} else if (body[checked_cast<size_t>(k)].type == '+') {
new_side.push_back({'+', body[checked_cast<size_t>(k)].text});
++k;
} else {
++k;
}
}
int oe = oc == 0 ? os : os + oc - 1;
int ne = nc == 0 ? ns : ns + nc - 1;
result += "***************\n";
result += std::format("*** {},{} ****\n", os, oe);
bool has_old = false;
for (auto &[p, t] : old_side) if (p != ' ') { has_old = true; break; }
if (has_old)
for (auto &[p, t] : old_side)
result += std::string(1, p) + " " + t + "\n";
result += std::format("--- {},{} ----\n", ns, ne);
bool has_new = false;
for (auto &[p, t] : new_side) if (p != ' ') { has_new = true; break; }
if (has_new)
for (auto &[p, t] : new_side)
result += std::string(1, p) + " " + t + "\n";
}
return result;
}
static constexpr std::string_view SNAPSHOT_PATCH = ".snap";
static bool is_placeholder_copy(std::string_view path)
{
return file_exists(path) && read_file(path).empty();
}
// Parse QUILT_DIFF_OPTS and extract context line count if present.
// Returns the context line count (-1 if not specified in opts).
static int parse_diff_opts_context(std::span<const std::string> opts)
{
for (ptrdiff_t i = 0; i < std::ssize(opts); ++i) {
const auto &o = opts[checked_cast<size_t>(i)];
if (o.starts_with("-U") && std::ssize(o) > 2) {
return checked_cast<int>(parse_int(std::string_view(o).substr(2)));
}
if (o == "-U" && i + 1 < std::ssize(opts)) {
return checked_cast<int>(parse_int(opts[checked_cast<size_t>(i + 1)]));
}
}
return -1;
}
// p_format: "ab" for a/b labels, "0" for bare filenames, "1" (default) for dir.orig/dir
// Format a file modification time as "YYYY-MM-DD HH:MM:SS.000000000 +HHMM".
static std::string format_file_timestamp(std::string_view path) {
int64_t mt = file_mtime(path);
if (mt <= 0) return "";
DateTime dt = local_time(mt);
int off_h = dt.utc_offset / 3600;
int off_m = (std::abs(dt.utc_offset) % 3600) / 60;
return std::format("\t{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}.000000000 {:+03d}{:02d}",
dt.year, dt.month, dt.day,
dt.hour, dt.min, dt.sec, off_h, off_m);
}
static std::string generate_path_diff(const QuiltState &q,
std::string_view file,
std::string_view old_path,
bool old_placeholder,
std::string_view new_path,
bool new_placeholder,
std::string_view p_format = "1",
bool reverse = false,
std::span<const std::string> diff_cmd_base = {},
int context_lines = 3,
DiffFormat diff_format = DiffFormat::unified,
bool no_timestamps = false,
DiffAlgorithm diff_algorithm = DiffAlgorithm::myers) {
bool old_missing = old_path.empty() || !file_exists(old_path) ||
(old_placeholder && is_placeholder_copy(old_path));
bool new_missing = new_path.empty() || !file_exists(new_path) ||
(new_placeholder && is_placeholder_copy(new_path));
// Detect binary files (null bytes in first 8 KB)
auto is_binary = [](std::string_view path) {
std::string data = read_file(path);
size_t check_len = data.size() < 8192 ? data.size() : 8192;
return data.find('\0', 0) < check_len;
};
if ((!old_missing && is_binary(old_path)) ||
(!new_missing && is_binary(new_path))) {
return "Binary files differ\n";
}
std::string old_arg = old_missing ? "/dev/null" : std::string(old_path);
std::string new_arg = new_missing ? "/dev/null" : std::string(new_path);
std::string old_label;
std::string new_label;
if (p_format == "ab") {
old_label = "a/" + std::string(file);
new_label = "b/" + std::string(file);
} else if (p_format == "0") {
old_label = std::string(file) + ".orig";
new_label = std::string(file);
} else {
std::string work_base = basename(q.work_dir);
old_label = work_base + ".orig/" + std::string(file);
new_label = work_base + "/" + std::string(file);
}
if (old_missing) {
old_label = "/dev/null";
}
if (new_missing) {
new_label = "/dev/null";
}
// Append file timestamps unless suppressed
if (!no_timestamps) {
if (!old_missing)
old_label += format_file_timestamp(old_path);
if (!new_missing)
new_label += format_file_timestamp(new_path);
}
if (reverse) {
std::swap(old_arg, new_arg);
}
// Use built-in diff when no external diff utility is specified
if (diff_cmd_base.empty()) {
int ctx = context_lines;
// QUILT_DIFF_OPTS may override context lines
auto extra_diff_opts = shell_split(get_env("QUILT_DIFF_OPTS"));
int opts_ctx = parse_diff_opts_context(extra_diff_opts);
if (opts_ctx >= 0) ctx = opts_ctx;
DiffResult result = builtin_diff(old_arg, new_arg, ctx,
old_label, new_label, diff_format,
diff_algorithm);
return result.output;
}
// External diff utility path
std::vector<std::string> cmd_argv(diff_cmd_base.begin(), diff_cmd_base.end());
auto extra_diff_opts = shell_split(get_env("QUILT_DIFF_OPTS"));
for (const auto &opt : extra_diff_opts) {
cmd_argv.push_back(opt);
}
cmd_argv.push_back("--label");
cmd_argv.push_back(old_label);
cmd_argv.push_back("--label");
cmd_argv.push_back(new_label);
cmd_argv.push_back(old_arg);
cmd_argv.push_back(new_arg);
ProcessResult result = run_cmd(cmd_argv);
if (result.exit_code == 2) {
return {};
}
return result.out;
}
static std::string generate_file_diff(const QuiltState &q, std::string_view patch,
std::string_view file,
std::string_view p_format = "1",
bool reverse = false,
std::span<const std::string> diff_cmd_base = {},
int context_lines = 3,
DiffFormat diff_format = DiffFormat::unified,
bool no_timestamps = false,
DiffAlgorithm diff_algorithm = DiffAlgorithm::myers) {
std::string backup_path = path_join(pc_patch_dir(q, patch), file);
std::string working_path = path_join(q.work_dir, file);
return generate_path_diff(q, file, backup_path, true, working_path, false,
p_format, reverse, diff_cmd_base,
context_lines, diff_format, no_timestamps,
diff_algorithm);
}
static std::map<std::string, std::string> split_patch_by_file(std::string_view content) {
std::map<std::string, std::string> sections;
auto lines = split_lines(content);
std::string current_file;
std::string current_section;
auto flush = [&]() {
if (!current_file.empty() && !current_section.empty()) {
sections[current_file] = current_section;
}
current_file.clear();
current_section.clear();
};
for (const auto &line : lines) {
if (line.starts_with("Index:") || line.starts_with("diff ")) {
flush();
current_section += line + "\n";
} else if (line.starts_with("+++ ")) {
// Extract filename from +++ line
std::string_view rest = std::string_view(line).substr(4);
if (rest.starts_with("/dev/null")) {
// File deletion: current_file already set from --- line
} else {
// Strip b/ prefix and trailing tab/timestamp
if (rest.starts_with("b/")) rest = rest.substr(2);
auto tab = str_find(rest, '\t');
if (tab >= 0) rest = rest.substr(0, checked_cast<size_t>(tab));
// Strip leading directory component (e.g., "dir.orig/")
auto slash = str_find(rest, '/');
if (slash >= 0) {
current_file = trim(rest.substr(checked_cast<size_t>(slash + 1)));
} else {
current_file = trim(rest);
}
}
current_section += line + "\n";
} else if (line.starts_with("===")) {
current_section += line + "\n";
} else if (line.starts_with("--- ")) {
// For file deletions (+++ /dev/null), we get the name from ---
if (current_file.empty()) {
std::string_view rest = std::string_view(line).substr(4);
if (!rest.starts_with("/dev/null")) {
if (rest.starts_with("a/")) rest = rest.substr(2);
auto tab = str_find(rest, '\t');
if (tab >= 0) rest = rest.substr(0, checked_cast<size_t>(tab));
auto slash = str_find(rest, '/');
if (slash >= 0) {
current_file = trim(rest.substr(checked_cast<size_t>(slash + 1)));
} else {
current_file = trim(rest);
}
}
}
current_section += line + "\n";
} else {
current_section += line + "\n";
}
}
flush();
return sections;
}
static void append_unique_files(std::vector<std::string> &dst,
std::set<std::string> &seen,
std::span<const std::string> src) {
for (const auto &file : src) {
if (seen.insert(file).second) {
dst.push_back(file);
}
}
}
static std::vector<std::string> collect_files_for_patches(
const QuiltState &q, std::span<const std::string> patches) {
std::vector<std::string> files;
std::set<std::string> seen;
for (const auto &patch : patches) {
append_unique_files(files, seen, files_in_patch(q, patch));
}
return files;
}
static std::vector<std::string> patch_range_for_diff(const QuiltState &q,
std::string_view last_patch) {
if (last_patch.empty()) {
return q.applied;
}
std::vector<std::string> patches;
for (const auto &patch : q.applied) {
patches.push_back(patch);
if (patch == last_patch) {
return patches;
}
}
return {std::string(last_patch)};
}
static std::string first_patch_for_file(const QuiltState &q,
std::span<const std::string> patches,
std::string_view file) {
for (const auto &patch : patches) {
auto tracked = files_in_patch(q, patch);
if (std::ranges::find(tracked, file) != tracked.end()) {
return patch;
}
}
return "";
}
static void apply_file_filter(std::vector<std::string> &tracked,
std::span<const std::string> file_filter) {
if (file_filter.empty()) {
return;
}
std::vector<std::string> filtered;
for (const auto &tracked_file : tracked) {
for (const auto &wanted_file : file_filter) {
if (tracked_file == wanted_file) {
filtered.push_back(tracked_file);
break;
}
}
}
tracked = std::move(filtered);
}
int cmd_snapshot(QuiltState &q, int argc, char **argv) {
bool remove_snapshot = false;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-d") {
remove_snapshot = true;
continue;
}
err_line("Usage: quilt snapshot [-d]");
return 1;
}
std::string snap_dir = pc_patch_dir(q, SNAPSHOT_PATCH);
if (is_directory(snap_dir) && !delete_dir_recursive(snap_dir)) {
err_line("Failed to remove " + snap_dir);
return 1;
}
if (remove_snapshot) {
return 0;
}
if (!ensure_pc_dir(q)) {
return 1;
}
if (!make_dirs(snap_dir)) {
err_line("Failed to create " + snap_dir);
return 1;
}
auto tracked = collect_files_for_patches(q, q.applied);
for (const auto &file : tracked) {
if (!backup_file(q, SNAPSHOT_PATCH, file)) {
err_line("Failed to snapshot " + file);
return 1;
}
}
return 0;
}
// Built-in diffstat: parse unified diff, produce a summary matching
// the output format of the external diffstat(1) utility.
static std::string generate_diffstat(std::string_view diff)
{
struct FileStat {
std::string name;
ptrdiff_t added = 0;
ptrdiff_t removed = 0;
};
std::vector<FileStat> stats;
auto lines = split_lines(diff);
for (ptrdiff_t i = 0; i < std::ssize(lines); ++i) {
const auto &line = lines[checked_cast<size_t>(i)];
// Detect file header: "--- a/file" followed by "+++ b/file"
if (line.starts_with("--- ") &&
i + 1 < std::ssize(lines) &&
lines[checked_cast<size_t>(i + 1)].starts_with("+++ ")) {
const auto &plus_line = lines[checked_cast<size_t>(i + 1)];
// Extract filename from +++ line, strip "b/" prefix
auto name = plus_line.substr(4);
// Strip trailing timestamp (tab-separated)
auto tab = str_find(name, '\t');
if (tab >= 0) name = name.substr(0, checked_cast<size_t>(tab));
// Strip one leading path component (a/ or b/ prefix)
auto slash = str_find(name, '/');
if (slash >= 0) name = name.substr(checked_cast<size_t>(slash + 1));
// /dev/null means new or deleted file — use --- line instead
if (name == "dev/null" || plus_line.substr(4).starts_with("/dev/null")) {
name = lines[checked_cast<size_t>(i)].substr(4);
tab = str_find(name, '\t');
if (tab >= 0) name = name.substr(0, checked_cast<size_t>(tab));
slash = str_find(name, '/');
if (slash >= 0) name = name.substr(checked_cast<size_t>(slash + 1));
}
stats.push_back({std::string(name), 0, 0});
i += 1; // skip +++ line
continue;
}
if (stats.empty()) continue;
if (line.starts_with("+") && !line.starts_with("+++"))
stats.back().added++;
else if (line.starts_with("-") && !line.starts_with("---"))
stats.back().removed++;
}
if (stats.empty()) return {};
// Leading "---" separator (matches git format-patch / original quilt)
std::string result = "---\n";
// Find max filename width and max change count
ptrdiff_t max_name = 0;
ptrdiff_t max_changes = 0;
for (const auto &s : stats) {
max_name = std::max(max_name, std::ssize(s.name));
max_changes = std::max(max_changes, s.added + s.removed);
}
// Format change count to find its width (minimum 4, matching diffstat)
auto num_width = std::max(std::ssize(std::to_string(max_changes)),
static_cast<ptrdiff_t>(4));
// Bar graph width: fit in ~72 columns after " name | num "
// 1 (leading space) + max_name + 3 (" | ") + num_width + 1 (space)
ptrdiff_t used = 1 + max_name + 3 + num_width + 1;
ptrdiff_t bar_width = std::max(static_cast<ptrdiff_t>(1),
static_cast<ptrdiff_t>(72) - used);
// Scale factor for bar graph
double scale = (max_changes > bar_width)
? static_cast<double>(bar_width) / static_cast<double>(max_changes)
: 1.0;
ptrdiff_t total_added = 0, total_removed = 0;
ptrdiff_t total_files = std::ssize(stats);
for (const auto &s : stats) {
total_added += s.added;
total_removed += s.removed;
ptrdiff_t changes = s.added + s.removed;
ptrdiff_t plus_bars = static_cast<ptrdiff_t>(
static_cast<double>(s.added) * scale + 0.5);
ptrdiff_t minus_bars = static_cast<ptrdiff_t>(
static_cast<double>(s.removed) * scale + 0.5);
// Ensure at least 1 bar for non-zero counts
if (s.added > 0 && plus_bars == 0) plus_bars = 1;
if (s.removed > 0 && minus_bars == 0) minus_bars = 1;
// Cap total bars at scaled width
ptrdiff_t total_bars = plus_bars + minus_bars;
ptrdiff_t limit = static_cast<ptrdiff_t>(
static_cast<double>(changes) * scale + 0.5);
if (limit < 1 && changes > 0) limit = 1;
if (total_bars > limit) {
// Reduce the larger portion
if (plus_bars > minus_bars)
plus_bars = limit - minus_bars;
else
minus_bars = limit - plus_bars;
}
result += ' ';
result += s.name;
for (ptrdiff_t j = std::ssize(s.name); j < max_name; ++j)
result += ' ';
result += " | ";
auto num_str = std::to_string(changes);
for (ptrdiff_t j = std::ssize(num_str); j < num_width; ++j)
result += ' ';
result += num_str;
result += ' ';
for (ptrdiff_t j = 0; j < plus_bars; ++j) result += '+';
for (ptrdiff_t j = 0; j < minus_bars; ++j) result += '-';
result += '\n';
}
// Summary line
result += ' ';
result += std::to_string(total_files);
result += (total_files == 1) ? " file changed" : " files changed";
if (total_added > 0) {
result += ", ";
result += std::to_string(total_added);
result += (total_added == 1) ? " insertion(+)" : " insertions(+)";
}
if (total_removed > 0) {
result += ", ";
result += std::to_string(total_removed);
result += (total_removed == 1) ? " deletion(-)" : " deletions(-)";
}
result += '\n';
return result;
}
// Remove an existing diffstat section from a patch header.
// Detects "---" separator followed by " file | N ++--" lines ending
// with a "N file(s) changed" summary line.
static std::string remove_diffstat_section(std::string_view header) {
auto lines = split_lines(header);
std::string result;
for (ptrdiff_t i = 0; i < std::ssize(lines); ++i) {
const auto &line = lines[checked_cast<size_t>(i)];
// Detect "---" separator followed by diffstat, or bare diffstat
ptrdiff_t ds_start = i;
if (line == "---" && i + 1 < std::ssize(lines)) {
ds_start = i + 1;
}
const auto &first = lines[checked_cast<size_t>(ds_start)];
if (!first.empty() && first[0] == ' ' &&
str_find(first, '|') >= 0) {
// Look ahead to confirm this is a diffstat block
bool found_summary = false;
ptrdiff_t summary_end = -1;
for (ptrdiff_t j = ds_start; j < std::ssize(lines); ++j) {
const auto &l = lines[checked_cast<size_t>(j)];
if (l.find("changed") != std::string::npos &&
l.find("file") != std::string::npos) {
found_summary = true;
summary_end = j;
break;
}
// If we hit an empty line or non-diffstat line, stop
if (l.empty() || (l[0] != ' ' && str_find(l, '|') < 0))
break;
}
if (found_summary) {
// Skip the entire diffstat block including summary
i = summary_end;
// Also skip a trailing blank line after diffstat
if (i + 1 < std::ssize(lines) && lines[checked_cast<size_t>(i + 1)].empty())
i++;
continue;
}
}
result += line;
result += '\n';
}
return result;
}
// Strip trailing whitespace from diff output lines.
// Returns the cleaned diff and emits warnings to stderr.
int cmd_refresh(QuiltState &q, int argc, char **argv) {
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
// Parse options
std::string patch;
std::string p_format;
bool explicit_p = false;
int i = 1;
bool no_timestamps = !get_env("QUILT_NO_DIFF_TIMESTAMPS").empty();
bool no_index = !get_env("QUILT_NO_DIFF_INDEX").empty();
bool sort_files = true;
bool force = false;
std::string diff_type;
std::string context_num;
bool opt_fork = false;
std::string fork_name;
bool opt_diffstat = false;
bool opt_backup = false;
bool opt_strip_whitespace = false;
DiffAlgorithm diff_algorithm = DiffAlgorithm::myers;
{
auto env_algo = get_env("QUILT_DIFF_ALGORITHM");
if (!env_algo.empty()) {
auto parsed = parse_diff_algorithm(env_algo);
if (!parsed) {
err("Unknown diff algorithm: "); err_line(env_algo);
return 1;
}
diff_algorithm = *parsed;
}
}
while (i < argc) {
std::string_view arg = argv[i];
if (arg == "-p" && i + 1 < argc) {
p_format = std::string(argv[i + 1]);
explicit_p = true;
i += 2;
continue;
}
if (arg.starts_with("-p") && std::ssize(arg) > 2) {
p_format = std::string(arg.substr(2));
explicit_p = true;
i += 1;
continue;
}
if (arg == "-f") {
force = true;
i += 1;
continue;
}
if (arg == "-u") {
diff_type = "u";
context_num.clear();
i += 1;
continue;
}
if (arg.starts_with("-U")) {
diff_type = "U";
if (arg == "-U" && i + 1 < argc) {
context_num = argv[i + 1];
i += 2;
} else {
context_num = std::string(arg.substr(2));
i += 1;
}
continue;
}
if (arg == "-c") {
diff_type = "c";
context_num.clear();
i += 1;
continue;
}
if (arg.starts_with("-C")) {
diff_type = "C";
if (arg == "-C" && i + 1 < argc) {
context_num = argv[i + 1];
i += 2;
} else {
context_num = std::string(arg.substr(2));
i += 1;
}
continue;
}
if (arg.starts_with("-z")) {
opt_fork = true;
if (std::ssize(arg) > 2) {
fork_name = strip_patches_prefix(q, arg.substr(2));
}
i += 1;
continue;
}
if (arg == "--no-timestamps" || arg == "--no-timestamp") {
no_timestamps = true;
i += 1;
continue;
}
if (arg == "--no-index") {
no_index = true;
i += 1;
continue;
}
if (arg == "--sort") {
sort_files = true;
i += 1;
continue;
}
if (arg == "--diffstat") {
opt_diffstat = true;
i += 1;
continue;
}
if (arg == "--backup") {
opt_backup = true;
i += 1;
continue;
}
if (arg == "--strip-trailing-whitespace") {
opt_strip_whitespace = true;
i += 1;
continue;
}
if (arg.starts_with("--diff-algorithm=")) {
auto name = arg.substr(17);
auto algo = parse_diff_algorithm(name);
if (!algo) {
err("Unknown diff algorithm: "); err_line(name);
return 1;
}
diff_algorithm = *algo;
i += 1;
continue;
}
if (arg == "--diff-algorithm" && i + 1 < argc) {
std::string_view name = argv[i + 1];
auto algo = parse_diff_algorithm(name);
if (!algo) {
err("Unknown diff algorithm: "); err_line(name);
return 1;
}
diff_algorithm = *algo;
i += 2;
continue;
}
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
// Non-option: patch name
if (patch.empty()) {
patch = arg;
}
i += 1;
}
if (patch.empty()) {
patch = q.applied.back();
}
if (!explicit_p) {
p_format = q.get_p_format(patch);
}
// Compute diff format and context lines
DiffFormat diff_format = DiffFormat::unified;
int ctx_lines = 3;
if (diff_type == "c") {
diff_format = DiffFormat::context;
} else if (diff_type == "C") {
diff_format = DiffFormat::context;
ctx_lines = checked_cast<int>(parse_int(context_num));
} else if (diff_type == "U") {
ctx_lines = checked_cast<int>(parse_int(context_num));
}
// Fork before refresh if -z was given
// Unlike standalone "fork" (which replaces the original), refresh -z
// inserts a new patch *after* the original and refreshes the fork.
// The original patch keeps its content. The fork captures only the
// delta between the original's refreshed state and the current working
// tree.
bool did_fork = false;
if (opt_fork) {
if (patch != q.applied.back()) {
err_line("Can only use -z with the topmost applied patch");
return 1;
}
std::string old_name(patch);
// Generate fork name
std::string new_name;
if (!fork_name.empty()) {
new_name = fork_name;
} else {
auto dot = str_rfind(old_name, '.');
if (dot > 0) {
new_name = old_name.substr(0, checked_cast<size_t>(dot)) + "-2" + old_name.substr(checked_cast<size_t>(dot));
} else {
new_name = old_name + "-2";
}
}
if (q.find_in_series(new_name)) {
err("Patch "); err(new_name); err_line(" already exists in series");
return 1;
}
auto idx = q.find_in_series(old_name);
if (!idx) {
err("Patch "); err(old_name); err_line(" is not in series");
return 1;
}
// Create the fork's .pc/ directory with backups representing the
// file state *after* the original patch (i.e., the intermediate
// state between the two patches). Reconstruct this by applying
// the original patch to the backup copies in an in-memory FS.
std::string old_pc = pc_patch_dir(q, old_name);
std::string new_pc = pc_patch_dir(q, new_name);
make_dirs(new_pc);
std::string orig_patch_path = path_join(q.work_dir, q.patches_dir, old_name);
std::string orig_patch_content = read_file(orig_patch_path);
int orig_strip = q.get_strip_level(old_name);
auto orig_files = files_in_patch(q, old_name);
// Build in-memory FS from backup copies, apply original patch
std::map<std::string, std::string> memfs;
for (const auto &file : orig_files) {
std::string backup_path = path_join(old_pc, file);
if (file_exists(backup_path) && !is_placeholder_copy(backup_path)) {
memfs[file] = read_file(backup_path);
}
}
if (!orig_patch_content.empty()) {
PatchOptions popts;
popts.strip_level = orig_strip;
popts.quiet = true;
popts.fs = &memfs;
if (q.patch_reversed.contains(old_name)) popts.reverse = true;
builtin_patch(orig_patch_content, popts);
}
// Write the intermediate states to the fork's .pc/ directory
for (const auto &file : orig_files) {
std::string fork_backup = path_join(new_pc, file);
std::string fork_dir = dirname(fork_backup);
if (!is_directory(fork_dir)) make_dirs(fork_dir);
auto it = memfs.find(file);
if (it != memfs.end()) {
write_file(fork_backup, it->second);
} else {
// File was deleted by patch or not present — write empty placeholder
write_file(fork_backup, "");
}
}
// Write .timestamp for the fork
write_file(path_join(new_pc, ".timestamp"), "");
// Migrate per-patch metadata to fork
auto sl_it = q.patch_strip_level.find(old_name);
if (sl_it != q.patch_strip_level.end()) {
q.patch_strip_level[new_name] = sl_it->second;
}
if (q.patch_reversed.contains(old_name)) {
q.patch_reversed.insert(new_name);
}
// Insert new patch after original in series
q.series.insert(q.series.begin() + *idx + 1, new_name);
std::string series_abs = path_join(q.work_dir, q.series_file);
if (!write_series(series_abs, q.series, q.patch_strip_level, q.patch_reversed)) {
err_line("Failed to write series file.");
return 1;
}
// Update applied: add fork after original
q.applied.push_back(new_name);
std::string applied_path = path_join(q.work_dir, q.pc_dir, "applied-patches");
write_applied(applied_path, q.applied);
out_line("Fork of patch " + patch_path_display(q, old_name) +
" created as " + patch_path_display(q, new_name));
patch = new_name;
// Suppress the "Refreshed patch" message; the fork message is sufficient
did_fork = true;
}
// Compute shadowed files (files modified by patches above this one)
// For each shadowed file, record the first patch above that tracks it
// (needed to find its backup as the "new" side of the diff).
std::set<std::string> shadowed;
std::map<std::string, std::string> shadow_next_patch;
if (patch != q.applied.back()) {
bool above = false;
for (const auto &a : q.applied) {
if (above) {
auto above_files = files_in_patch(q, a);
for (const auto &f : above_files) {
if (!shadowed.contains(f)) {
shadow_next_patch[f] = a;
}
shadowed.insert(f);
}
}
if (a == patch) above = true;
}
}
if (!shadowed.empty() && !force) {
err("More recent patches modify files in patch ");
err(patch_path_display(q, patch)); err_line(". Enforce refresh with -f.");
return 1;
}
// Get files tracked by this patch
auto tracked = files_in_patch(q, patch);
if (sort_files) {
std::ranges::sort(tracked);
}
// Read existing patch file for header
std::string patch_file = path_join(q.work_dir, q.patches_dir, patch);
std::string old_content;
std::string header;
if (file_exists(patch_file)) {
old_content = read_file(patch_file);
header = read_patch_header(patch_file);
}
// Backup old patch file if requested
if (opt_backup && file_exists(patch_file)) {
copy_file(patch_file, patch_file + "~");
}
// Generate diffs
std::string work_base = basename(q.work_dir);
std::string patch_content = header;
for (const auto &file : tracked) {
if (shadowed.contains(file)) {
// Diff this patch's backup against the next patch's backup
auto it = shadow_next_patch.find(file);
if (it != shadow_next_patch.end()) {
std::string this_backup = path_join(pc_patch_dir(q, patch), file);
std::string next_backup = path_join(pc_patch_dir(q, it->second), file);
std::string diff_out = generate_path_diff(q, file,
this_backup, true, next_backup, true,
p_format, false, {}, ctx_lines, diff_format, no_timestamps,
diff_algorithm);
if (!diff_out.empty()) {
if (!no_index) {
std::string idx_name;
if (p_format == "0") idx_name = file;
else if (p_format == "ab") idx_name = "b/" + file;
else idx_name = basename(q.work_dir) + "/" + file;
patch_content += "Index: " + idx_name + "\n";
patch_content += "===================================================================\n";
}
patch_content += diff_out;
if (!patch_content.empty() && patch_content.back() != '\n') {
patch_content += '\n';
}
}
}
continue;
}
// Strip trailing whitespace from lines modified by this patch
if (opt_strip_whitespace) {
std::string working_path = path_join(q.work_dir, file);
if (file_exists(working_path)) {
// Find which lines are modified by diffing backup vs working
std::set<int> modified_lines;
std::string diff_check = generate_file_diff(
q, patch, file, "1", false, {}, 0,
DiffFormat::unified, true, diff_algorithm);
if (!diff_check.empty()) {
auto dlines = split_lines(diff_check);
int new_lineno = 0;
for (const auto &dl : dlines) {
if (dl.starts_with("---") || dl.starts_with("+++")) {
continue;
}
if (dl.starts_with("@@")) {
// Parse @@ -X,Y +N,M @@
auto plus = str_find(dl, '+');
if (plus >= 0) {
auto comma = str_find(
std::string_view(dl).substr(
checked_cast<size_t>(plus)), ',');
std::string_view num_str;
if (comma >= 0) {
num_str = std::string_view(dl).substr(
checked_cast<size_t>(plus) + 1,
checked_cast<size_t>(comma) - 1);
} else {
auto sp = str_find(
std::string_view(dl).substr(
checked_cast<size_t>(plus)), ' ');
if (sp >= 0) {
num_str = std::string_view(dl).substr(
checked_cast<size_t>(plus) + 1,
checked_cast<size_t>(sp) - 1);
}
}
if (!num_str.empty()) {
int n = 0;
std::from_chars(num_str.data(),
num_str.data() + num_str.size(), n);
new_lineno = n - 1; // will be incremented
}
}
} else if (dl.starts_with("+")) {
new_lineno++;
modified_lines.insert(new_lineno);
} else if (!dl.starts_with("-")) {
new_lineno++; // context line
}
}
}
std::string content = read_file(working_path);
std::string stripped;
auto lines = split_lines(content);
int lineno = 0;
for (const auto &line : lines) {
lineno++;
std::string_view l = line;
bool is_modified = modified_lines.contains(lineno);
auto end = l.find_last_not_of(" \t");
if (is_modified && end == std::string::npos) {
if (!l.empty()) {
out_line("Removing trailing whitespace from line "
+ std::to_string(lineno) + " of " + file);
}
stripped += '\n';
} else if (is_modified &&
static_cast<ptrdiff_t>(end) + 1 < std::ssize(l)) {
out_line("Removing trailing whitespace from line "
+ std::to_string(lineno) + " of " + file);
stripped += l.substr(0, end + 1);
stripped += '\n';
} else {
stripped += l;
stripped += '\n';
}
}
if (stripped != content) {
write_file(working_path, stripped);
}
}
}
std::string diff_out = generate_file_diff(q, patch, file, p_format,
false, {}, ctx_lines,
diff_format, no_timestamps,
diff_algorithm);
if (diff_out.starts_with("Binary files ")) {
err("Diff failed on file '"); err(file); err_line("', aborting");
return 1;
}
if (!diff_out.empty()) {
if (!no_index) {
std::string idx_name;
if (p_format == "0") idx_name = file;
else if (p_format == "ab") idx_name = "b/" + file;
else idx_name = work_base + "/" + file;
patch_content += "Index: " + idx_name + "\n";
patch_content += "===================================================================\n";
}
patch_content += diff_out;
// Ensure trailing newline
if (!patch_content.empty() && patch_content.back() != '\n') {
patch_content += '\n';
}
}
}
// Add diffstat to header if requested
if (opt_diffstat) {
std::string diff_portion = patch_content.substr(checked_cast<size_t>(std::ssize(header)));
if (!diff_portion.empty()) {
std::string ds_out = generate_diffstat(diff_portion);
if (!ds_out.empty()) {
std::string clean_header = remove_diffstat_section(header);
// Remove trailing blank lines from header
while (std::ssize(clean_header) > 1 &&
clean_header[checked_cast<size_t>(std::ssize(clean_header) - 1)] == '\n' &&
clean_header[checked_cast<size_t>(std::ssize(clean_header) - 2)] == '\n') {
clean_header.pop_back();
}
patch_content = clean_header;
if (!patch_content.empty() && patch_content.back() != '\n')
patch_content += '\n';
patch_content += ds_out;
if (!ds_out.empty() && ds_out.back() != '\n')
patch_content += '\n';
patch_content += '\n';
patch_content += diff_portion;
}
}
}
// Check if patch has no diff hunks
bool has_diff = false;
for (auto &line : split_lines(patch_content)) {
if (line.starts_with("--- ") || line.starts_with("diff ")) {
has_diff = true;
break;
}
}
// Check if patch content is unchanged (only skip write if file exists)
if (patch_content == old_content && file_exists(patch_file)) {
if (!has_diff) {
out("Nothing in patch "); out_line(patch_path_display(q, patch));
} else {
out("Patch "); out(patch_path_display(q, patch));
out_line(" is unchanged");
}
return 0;
}
// Ensure patches directory exists
std::string patch_dir = dirname(patch_file);
if (!is_directory(patch_dir)) {
make_dirs(patch_dir);
}
// Write the patch file
if (!write_file(patch_file, patch_content)) {
err_line("Failed to write patch file " + patch_file);
return 1;
}
// Update .timestamp
write_file(path_join(pc_patch_dir(q, patch), ".timestamp"), "");
// Clear .needs_refresh marker if present
std::string nr = path_join(pc_patch_dir(q, patch), ".needs_refresh");
if (file_exists(nr)) {
delete_file(nr);
}
if (!did_fork) {
if (!has_diff) {
out("Nothing in patch "); out_line(patch_path_display(q, patch));
} else {
out("Refreshed patch "); out_line(patch_path_display(q, patch));
}
}
return 0;
}
int cmd_diff(QuiltState &q, int argc, char **argv) {
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
// Parse options
std::string_view patch;
std::string p_format;
bool explicit_p = false;
std::vector<std::string> file_filter;
bool no_timestamps = !get_env("QUILT_NO_DIFF_TIMESTAMPS").empty();
bool no_index = !get_env("QUILT_NO_DIFF_INDEX").empty();
bool since_refresh = false;
bool against_snapshot = false;
bool reverse = false;
bool sort_files = true;
std::string diff_utility;
std::string combine_patch;
std::string diff_type = "u";
std::string context_num;
DiffAlgorithm diff_algorithm = DiffAlgorithm::myers;
{
auto env_algo = get_env("QUILT_DIFF_ALGORITHM");
if (!env_algo.empty()) {
auto parsed = parse_diff_algorithm(env_algo);
if (!parsed) {
err("Unknown diff algorithm: "); err_line(env_algo);
return 1;
}
diff_algorithm = *parsed;
}
}
int i = 1;
while (i < argc) {
std::string_view arg = argv[i];
if (arg == "-P" && i + 1 < argc) {
patch = strip_patches_prefix(q, argv[i + 1]);
i += 2;
continue;
}
if (arg == "-p" && i + 1 < argc) {
p_format = std::string(argv[i + 1]);
explicit_p = true;
i += 2;
continue;
}
if (arg.starts_with("-p") && std::ssize(arg) > 2) {
p_format = std::string(arg.substr(2));
explicit_p = true;
i += 1;
continue;
}
if (arg == "-u") {
diff_type = "u";
context_num.clear();
i += 1;
continue;
}
if (arg == "-c") {
diff_type = "c";
context_num.clear();
i += 1;
continue;
}
if (arg.starts_with("-C")) {
diff_type = "C";
if (arg == "-C" && i + 1 < argc) {
context_num = argv[i + 1];
i += 2;
} else {
context_num = std::string(arg.substr(2));
i += 1;
}
continue;
}
if (arg.starts_with("-U")) {
diff_type = "U";
if (arg == "-U" && i + 1 < argc) {
context_num = argv[i + 1];
i += 2;
} else {
context_num = std::string(arg.substr(2));
i += 1;
}
continue;
}
if (arg == "-z") {
since_refresh = true;
i += 1;
continue;
}
if (arg == "--snapshot") {
against_snapshot = true;
i += 1;
continue;
}
if (arg == "-R") {
reverse = true;
i += 1;
continue;
}
if (arg == "--no-timestamps" || arg == "--no-timestamp") {
no_timestamps = true;
i += 1;
continue;
}
if (arg == "--no-index") {
no_index = true;
i += 1;
continue;
}
if (arg == "--sort") {
sort_files = true;
i += 1;
continue;
}
if (arg == "--combine" && i + 1 < argc) {
combine_patch = argv[i + 1];
i += 2;
continue;
}
if (arg.starts_with("--combine=")) {
combine_patch = std::string(arg.substr(10));
i += 1;
continue;
}
if (arg.starts_with("--diff=")) {
diff_utility = std::string(arg.substr(7));
i += 1;
continue;
}
if (arg.starts_with("--diff-algorithm=")) {
auto name = arg.substr(17);
auto algo = parse_diff_algorithm(name);
if (!algo) {
err("Unknown diff algorithm: "); err_line(name);
return 1;
}
diff_algorithm = *algo;
i += 1;
continue;
}
if (arg == "--diff-algorithm" && i + 1 < argc) {
std::string_view name = argv[i + 1];
auto algo = parse_diff_algorithm(name);
if (!algo) {
err("Unknown diff algorithm: "); err_line(name);
return 1;
}
diff_algorithm = *algo;
i += 2;
continue;
}
if (arg == "--color" || arg.starts_with("--color=")) {
if (arg.starts_with("--color=")) {
auto val = arg.substr(8);
if (val != "always" && val != "auto" && val != "never") {
err("Invalid --color value: "); err_line(val);
return 1;
}
}
i += 1;
continue;
}
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
// Non-option: file name or patch name
if (arg[0] != '-') {
file_filter.push_back(subdir_path(q, arg));
}
i += 1;
}
if (patch.empty()) {
patch = q.applied.back();
}
if (!explicit_p) {
p_format = q.get_p_format(patch);
}
if (since_refresh && against_snapshot) {
err_line("Options `--snapshot' and `-z' cannot be combined.");
return 1;
}
if (!combine_patch.empty() && since_refresh) {
err_line("Options `--combine' and `-z' cannot be combined.");
return 1;
}
if (!combine_patch.empty() && against_snapshot) {
err_line("Options `--combine' and `--snapshot' cannot be combined.");
return 1;
}
// Determine diff format and context lines for builtin diff
DiffFormat diff_format = DiffFormat::unified;
int ctx_lines = 3;
if (diff_type == "c") {
diff_format = DiffFormat::context;
} else if (diff_type == "C") {
diff_format = DiffFormat::context;
ctx_lines = checked_cast<int>(parse_int(context_num));
} else if (diff_type == "U") {
ctx_lines = checked_cast<int>(parse_int(context_num));
}
// Build diff command base for external diff utility (empty = use builtin)
bool convert_to_context = false;
std::vector<std::string> diff_cmd_base;
if (!diff_utility.empty()) {
auto parts = split_on_whitespace(diff_utility);
for (auto &p : parts) diff_cmd_base.push_back(std::move(p));
// External diff: always request unified, convert to context in-process
convert_to_context = (diff_type == "c" || diff_type == "C");
if (diff_type == "U" || diff_type == "C") {
diff_cmd_base.push_back("-U");
diff_cmd_base.push_back(context_num);
} else {
diff_cmd_base.push_back("-u");
}
}
auto emit_diff = [&](std::string_view d) {
if (convert_to_context)
out(unified_to_context(d));
else
out(d);
};
// Resolve --combine patch name
std::string combine_start;
if (!combine_patch.empty()) {
if (combine_patch == "-") {
combine_start = q.applied.front();
} else {
combine_start = strip_patches_prefix(q, combine_patch);
}
}
auto patches = patch_range_for_diff(q, patch);
std::vector<std::string> tracked;
if (against_snapshot) {
std::string snap_dir = pc_patch_dir(q, SNAPSHOT_PATCH);
if (!is_directory(snap_dir)) {
err_line("No snapshot to diff against");
return 1;
}
std::set<std::string> seen;
append_unique_files(tracked, seen, files_in_patch(q, SNAPSHOT_PATCH));
append_unique_files(tracked, seen, collect_files_for_patches(q, patches));
} else if (!combine_start.empty()) {
// Collect files across the combine range
std::vector<std::string> combine_range;
bool in_range = false;
for (const auto &a : q.applied) {
if (a == combine_start) in_range = true;
if (in_range) combine_range.push_back(a);
if (a == patch) break;
}
tracked = collect_files_for_patches(q, combine_range);
} else {
tracked = files_in_patch(q, patch);
}
apply_file_filter(tracked, file_filter);
if (sort_files) {
std::ranges::sort(tracked);
}
std::string work_base = basename(q.work_dir);
if (since_refresh) {
// diff -z: show changes since last refresh.
// For each tracked file, reconstruct the "refreshed state" by applying
// the stored patch to the backup, then diff that against the working file.
std::string patch_file = path_join(q.work_dir, q.patches_dir, patch);
std::string stored_content;
std::map<std::string, std::string> stored_sections;
if (file_exists(patch_file)) {
stored_content = read_file(patch_file);
stored_sections = split_patch_by_file(stored_content);
}
// Create temp directory for reconstructing refreshed state
std::string tmp_dir = make_temp_dir();
if (tmp_dir.empty()) {
err_line("Failed to create temp directory");
return 1;
}
for (const auto &file : tracked) {
std::string backup_path = path_join(pc_patch_dir(q, patch), file);
std::string working_path = path_join(q.work_dir, file);
std::string tmp_file = path_join(tmp_dir, file);
// Ensure temp subdirectory exists
std::string tmp_file_dir = dirname(tmp_file);
if (!is_directory(tmp_file_dir)) {
make_dirs(tmp_file_dir);
}
// Copy backup to temp (empty backup = file didn't exist)
if (file_exists(backup_path)) {
std::string backup_content = read_file(backup_path);
write_file(tmp_file, backup_content);
} else {
write_file(tmp_file, "");
}
// Apply stored patch section to temp file
auto it = stored_sections.find(file);
if (it != stored_sections.end() && !it->second.empty()) {
// Build a minimal patch with correct paths for -p0
std::string mini_patch = "--- " + file + "\n+++ " + file + "\n";
// Extract just the hunk lines from the stored section
auto section_lines = split_lines(it->second);
bool in_hunk = false;
for (const auto &sl : section_lines) {
if (sl.starts_with("@@")) {
in_hunk = true;
mini_patch += sl + "\n";
} else if (in_hunk) {
mini_patch += sl + "\n";
}
}
if (in_hunk) {
std::string saved_cwd = get_cwd();
if (set_cwd(tmp_dir)) {
PatchOptions po;
po.strip_level = 0;
po.remove_empty = true;
po.quiet = true;
builtin_patch(mini_patch, po);
set_cwd(saved_cwd);
}
}
}
// Now diff the reconstructed "refreshed" file against working file
if (!file_exists(working_path) && !file_exists(tmp_file)) continue;
std::string old_label, new_label;
if (p_format == "ab") {
old_label = "a/" + file;
new_label = "b/" + file;
} else if (p_format == "0") {
old_label = file;
new_label = file;
} else {
old_label = work_base + ".orig/" + file;
new_label = work_base + "/" + file;
}
std::string old_f = tmp_file;
std::string new_f = working_path;
if (!file_exists(working_path)) new_f = "/dev/null";
if (reverse) {
std::swap(old_f, new_f);
}
std::string diff_out;
if (diff_cmd_base.empty()) {
// Use built-in diff
int ctx = ctx_lines;
auto extra_diff_opts = shell_split(get_env("QUILT_DIFF_OPTS"));
int opts_ctx = parse_diff_opts_context(extra_diff_opts);
if (opts_ctx >= 0) ctx = opts_ctx;
DiffResult dr = builtin_diff(old_f, new_f, ctx,
old_label, new_label, diff_format,
diff_algorithm);
diff_out = std::move(dr.output);
} else {
std::vector<std::string> diff_cmd = diff_cmd_base;
auto extra_diff_opts = shell_split(get_env("QUILT_DIFF_OPTS"));
for (const auto &opt : extra_diff_opts) diff_cmd.push_back(opt);
diff_cmd.push_back("--label");
diff_cmd.push_back(old_label);
diff_cmd.push_back("--label");
diff_cmd.push_back(new_label);
diff_cmd.push_back(old_f);
diff_cmd.push_back(new_f);
ProcessResult result = run_cmd(diff_cmd);
if (result.exit_code == 1) {
diff_out = std::move(result.out);
}
}
if (!diff_out.empty()) {
if (!no_index) {
out("Index: " + (p_format == "0" ? file : p_format == "ab" ? "b/" + file : work_base + "/" + file) + "\n");
out("===================================================================\n");
}
emit_diff(diff_out);
}
}
delete_dir_recursive(tmp_dir);
} else if (against_snapshot) {
for (const auto &file : tracked) {
std::string old_path = path_join(pc_patch_dir(q, SNAPSHOT_PATCH), file);
bool old_placeholder = true;
if (!file_exists(old_path)) {
std::string first_patch = first_patch_for_file(q, patches, file);
if (first_patch.empty()) {
continue;
}
old_path = path_join(pc_patch_dir(q, first_patch), file);
}
std::string new_path = path_join(q.work_dir, file);
bool new_placeholder = false;
std::string shadowing_patch = next_patch_for_file(q, patch, file);
if (!shadowing_patch.empty()) {
new_path = path_join(pc_patch_dir(q, shadowing_patch), file);
new_placeholder = true;
}
std::string diff_out = generate_path_diff(
q, file, old_path, old_placeholder, new_path, new_placeholder,
p_format, reverse, diff_cmd_base, ctx_lines, diff_format,
no_timestamps, diff_algorithm);
if (!diff_out.empty()) {
if (!no_index) {
out("Index: " + (p_format == "0" ? file : p_format == "ab" ? "b/" + file : work_base + "/" + file) + "\n");
out("===================================================================\n");
}
emit_diff(diff_out);
}
}
} else if (!combine_start.empty()) {
// --combine: diff backup from the earliest patch in range against working file
for (const auto &file : tracked) {
// Find the earliest patch in the combine range that tracks this file
std::string earliest;
bool in_range = false;
for (const auto &a : q.applied) {
if (a == combine_start) in_range = true;
if (in_range) {
auto fip = files_in_patch(q, a);
if (std::ranges::find(fip, file) != fip.end()) {
earliest = a;
break;
}
}
if (a == patch) break;
}
if (earliest.empty()) continue;
std::string old_path = path_join(pc_patch_dir(q, earliest), file);
std::string new_path = path_join(q.work_dir, file);
bool new_placeholder = false;
// If a patch above the range shadows this file, use its backup
std::string shadowing_patch = next_patch_for_file(q, patch, file);
if (!shadowing_patch.empty()) {
new_path = path_join(pc_patch_dir(q, shadowing_patch), file);
new_placeholder = true;
}
std::string diff_out = generate_path_diff(
q, file, old_path, true, new_path, new_placeholder,
p_format, reverse, diff_cmd_base, ctx_lines, diff_format,
no_timestamps, diff_algorithm);
if (!diff_out.empty()) {
if (!no_index) {
out("Index: " + (p_format == "0" ? file : p_format == "ab" ? "b/" + file : work_base + "/" + file) + "\n");
out("===================================================================\n");
}
emit_diff(diff_out);
}
}
} else {
// Warn if more recent patches modify files in this patch
bool warned_shadowing = false;
for (const auto &file : tracked) {
std::string shadowing = next_patch_for_file(q, patch, file);
if (!shadowing.empty() && !warned_shadowing) {
err("Warning: more recent patches modify files in patch ");
err_line(patch_path_display(q, patch));
warned_shadowing = true;
}
std::string old_path = path_join(pc_patch_dir(q, patch), file);
std::string new_path = path_join(q.work_dir, file);
bool new_placeholder = false;
// If a patch above this one shadows the file, use its backup
if (!shadowing.empty()) {
new_path = path_join(pc_patch_dir(q, shadowing), file);
new_placeholder = true;
}
std::string diff_out = generate_path_diff(
q, file, old_path, true, new_path, new_placeholder,
p_format, reverse, diff_cmd_base, ctx_lines, diff_format,
no_timestamps, diff_algorithm);
if (!diff_out.empty()) {
if (!no_index) {
out("Index: " + (p_format == "0" ? file : p_format == "ab" ? "b/" + file : work_base + "/" + file) + "\n");
out("===================================================================\n");
}
emit_diff(diff_out);
}
}
}
return 0;
}
int cmd_revert(QuiltState &q, int argc, char **argv) {
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
// Parse options
std::string_view patch = q.applied.back();
std::vector<std::string> files;
int i = 1;
while (i < argc) {
std::string_view arg = argv[i];
if (arg == "-P" && i + 1 < argc) {
patch = strip_patches_prefix(q, argv[i + 1]);
i += 2;
continue;
}
if (arg[0] != '-') {
files.push_back(subdir_path(q, arg));
} else {
err("Unrecognized option: "); err_line(arg);
return 1;
}
i += 1;
}
if (files.empty()) {
err_line("Usage: quilt revert [-P patch] file ...");
return 1;
}
if (!q.is_applied(patch)) {
err("Patch "); err(format_patch(q, patch)); err_line(" is not applied");
return 1;
}
// Check if any later applied patch also modifies these files
bool found_patch = false;
for (const auto &ap : q.applied) {
if (!found_patch) {
if (ap == patch) found_patch = true;
continue;
}
// ap is a patch applied after 'patch'
auto later_files = files_in_patch(q, ap);
for (const auto &file : files) {
for (const auto &lf : later_files) {
if (lf == file) {
err("File "); err(file);
err(" modified by patch ");
err_line(patch_path_display(q, ap));
return 1;
}
}
}
}
// Read the patch file to apply its hunks to backup content
std::string patch_file = path_join(q.work_dir, q.patches_dir, patch);
std::string patch_text = read_file(patch_file);
int strip_level = q.patch_strip_level.count(std::string(patch))
? q.patch_strip_level.at(std::string(patch)) : 1;
for (const auto &file : files) {
// Check if file is tracked by the patch
std::string backup_path = path_join(pc_patch_dir(q, patch), file);
if (!file_exists(backup_path)) {
err("File "); err(file); err(" is not in patch ");
err_line(patch_path_display(q, patch));
return 1;
}
// Build the clean post-patch state by applying patch to backup
std::string backup_content = read_file(backup_path);
std::map<std::string, std::string> memfs;
memfs[file] = backup_content;
PatchOptions opts;
opts.strip_level = strip_level;
opts.quiet = true;
opts.fs = &memfs;
builtin_patch(patch_text, opts);
std::string clean_content = memfs.count(file) ? memfs[file] : "";
// Check if current file matches clean state (unchanged)
std::string target = path_join(q.work_dir, file);
std::string current = file_exists(target) ? read_file(target) : "";
if (current == clean_content) {
out("File "); out(file);
out_line(" is unchanged");
continue;
}
// Write the clean post-patch state
if (clean_content.empty()) {
// Post-patch state is empty — either file was deleted by patch
// or didn't exist. Remove the working-tree copy.
delete_file(target);
out("Changes to "); out(file); out(" in patch ");
out(patch_path_display(q, patch)); out_line(" reverted");
continue;
}
std::string target_dir = dirname(target);
if (!is_directory(target_dir)) {
make_dirs(target_dir);
}
if (!write_file(target, clean_content)) {
err("Failed to restore "); err_line(file);
return 1;
}
out("Changes to "); out(file); out(" in patch ");
out(patch_path_display(q, patch)); out_line(" reverted");
}
return 0;
}
// === src/cmd_manage.cpp ===
// This is free and unencumbered software released into the public domain.
static bool write_series_checked(const QuiltState &q,
std::span<const std::string> series) {
std::string series_abs = path_join(q.work_dir, q.series_file);
if (!write_series(series_abs, series, q.patch_strip_level, q.patch_reversed)) {
err_line("Failed to write series file.");
return false;
}
return true;
}
static bool write_applied_checked(const QuiltState &q,
std::span<const std::string> applied) {
std::string applied_path = path_join(q.work_dir, q.pc_dir, "applied-patches");
if (!write_applied(applied_path, applied)) {
err_line("Failed to write applied-patches.");
return false;
}
return true;
}
static std::string extract_header(std::string_view content) {
std::string header;
auto lines = split_lines(content);
for (const auto &line : lines) {
if (line.starts_with("Index:") ||
line.starts_with("--- ") ||
line.starts_with("diff ") ||
line.starts_with("===")) {
break;
}
header += line;
header += '\n';
}
return header;
}
static std::string replace_header(std::string_view content, std::string_view new_header) {
std::string result;
auto lines = split_lines(content);
bool in_diff = false;
// Find where diffs start
ptrdiff_t diff_start = 0;
for (ptrdiff_t i = 0; i < std::ssize(lines); ++i) {
if (lines[checked_cast<size_t>(i)].starts_with("Index:") ||
lines[checked_cast<size_t>(i)].starts_with("--- ") ||
lines[checked_cast<size_t>(i)].starts_with("diff ") ||
lines[checked_cast<size_t>(i)].starts_with("===")) {
diff_start = i;
in_diff = true;
break;
}
}
result += std::string(new_header);
// Ensure header ends with newline if non-empty
if (!result.empty() && result.back() != '\n') {
result += '\n';
}
if (in_diff) {
for (ptrdiff_t i = diff_start; i < std::ssize(lines); ++i) {
result += lines[checked_cast<size_t>(i)];
result += '\n';
}
}
return result;
}
int cmd_delete(QuiltState &q, int argc, char **argv) {
bool opt_remove = false;
bool opt_backup = false;
bool opt_next = false;
std::string_view patch_arg;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-r") {
opt_remove = true;
} else if (arg == "--backup") {
opt_backup = true;
} else if (arg == "-n") {
opt_next = true;
} else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
} else {
patch_arg = strip_patches_prefix(q, arg);
}
}
std::string patch;
if (!patch_arg.empty()) {
patch = patch_arg;
} else if (opt_next) {
// Next unapplied patch
ptrdiff_t top_idx = q.top_index();
ptrdiff_t next_idx = top_idx + 1;
if (next_idx >= std::ssize(q.series)) {
err_line("No next patch");
return 1;
}
patch = q.series[checked_cast<size_t>(next_idx)];
} else {
// Topmost applied patch
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
patch = q.applied.back();
}
// Verify patch is in series
auto idx = q.find_in_series(patch);
if (!idx) {
err("Patch "); err(patch); err_line(" is not in series");
return 1;
}
// If patch is applied, only allow deleting the topmost patch
if (q.is_applied(patch)) {
if (patch != q.applied.back()) {
err("Patch "); err(patch_path_display(q, patch));
err_line(" is currently applied");
return 1;
}
// Pop the topmost patch silently (no per-file messages)
auto tracked = files_in_patch(q, patch);
out_line("Removing patch " + patch_path_display(q, patch));
for (const auto &f : tracked) {
restore_file(q, patch, f);
}
std::string pc_dir = pc_patch_dir(q, patch);
if (is_directory(pc_dir)) delete_dir_recursive(pc_dir);
q.applied.pop_back();
if (!write_applied_checked(q, q.applied)) return 1;
if (!q.applied.empty()) {
out_line("Now at patch " +
patch_path_display(q, q.applied.back()));
} else {
out_line("No patches applied");
}
}
// Remove from series
auto new_series = q.series;
new_series.erase(new_series.begin() + *idx);
if (!write_series_checked(q, new_series)) {
return 1;
}
q.series = std::move(new_series);
// Optionally remove the patch file
if (opt_remove) {
std::string patch_file = path_join(q.work_dir, q.patches_dir, patch);
if (opt_backup) {
std::string backup = patch_file + "~";
if (file_exists(patch_file) && !rename_path(patch_file, backup)) {
err_line("Failed to rename " + patch_file + " to " + backup);
return 1;
}
} else if (file_exists(patch_file) && !delete_file(patch_file)) {
err_line("Failed to delete " + patch_file);
return 1;
}
}
out_line("Removed patch " + patch_path_display(q, patch));
return 0;
}
int cmd_rename(QuiltState &q, int argc, char **argv) {
std::string old_patch;
std::string new_name;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-P" && i + 1 < argc) {
old_patch = strip_patches_prefix(q, argv[++i]);
} else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
} else {
new_name = strip_patches_prefix(q, arg);
}
}
// Default to top patch
if (old_patch.empty()) {
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
old_patch = q.applied.back();
}
if (new_name.empty()) {
err_line("Usage: quilt rename [-P patch] new_name");
return 1;
}
// Verify old patch exists in series
auto idx = q.find_in_series(old_patch);
if (!idx) {
err("Patch "); err(old_patch); err_line(" is not in series");
return 1;
}
// Verify new name doesn't exist in series
auto new_idx = q.find_in_series(new_name);
if (new_idx) {
err("Patch "); err(patch_path_display(q, new_name));
err_line(" exists already, please choose a different name");
return 1;
}
// Rename patch file
std::string old_file = path_join(q.work_dir, q.patches_dir, old_patch);
std::string new_file = path_join(q.work_dir, q.patches_dir, new_name);
bool renamed_patch_file = false;
if (file_exists(old_file)) {
// Ensure target directory exists
std::string new_dir = dirname(new_file);
if (!is_directory(new_dir)) {
if (!make_dirs(new_dir)) {
err_line("Failed to create " + new_dir);
return 1;
}
}
if (!rename_path(old_file, new_file)) {
err_line("Failed to rename " + old_file + " to " + new_file);
return 1;
}
renamed_patch_file = true;
}
// If patch is applied: rename in applied-patches and .pc/ dir
auto new_applied = q.applied;
bool renamed_pc_dir = false;
if (q.is_applied(old_patch)) {
for (auto &a : new_applied) {
if (a == old_patch) {
a = new_name;
break;
}
}
std::string old_pc = pc_patch_dir(q, old_patch);
std::string new_pc = pc_patch_dir(q, new_name);
if (is_directory(old_pc)) {
if (!rename_path(old_pc, new_pc)) {
if (renamed_patch_file) {
rename_path(new_file, old_file);
}
err_line("Failed to rename " + old_pc + " to " + new_pc);
return 1;
}
renamed_pc_dir = true;
}
}
// Migrate per-patch metadata before writing series
std::string old_key(old_patch);
auto sl_it = q.patch_strip_level.find(old_key);
int saved_strip = -1;
bool saved_reversed = false;
if (sl_it != q.patch_strip_level.end()) {
saved_strip = sl_it->second;
q.patch_strip_level[new_name] = sl_it->second;
q.patch_strip_level.erase(sl_it);
}
if (q.patch_reversed.erase(old_key)) {
saved_reversed = true;
q.patch_reversed.insert(new_name);
}
auto new_series = q.series;
new_series[checked_cast<size_t>(*idx)] = new_name;
if (!write_series_checked(q, new_series)) {
// Undo metadata migration
if (saved_strip >= 0) {
q.patch_strip_level[old_key] = saved_strip;
q.patch_strip_level.erase(new_name);
}
if (saved_reversed) {
q.patch_reversed.erase(new_name);
q.patch_reversed.insert(old_key);
}
if (renamed_pc_dir) {
rename_path(pc_patch_dir(q, new_name), pc_patch_dir(q, old_patch));
}
if (renamed_patch_file) {
rename_path(new_file, old_file);
}
return 1;
}
if (q.is_applied(old_patch) && !write_applied_checked(q, new_applied)) {
write_series_checked(q, q.series);
if (saved_strip >= 0) {
q.patch_strip_level[old_key] = saved_strip;
q.patch_strip_level.erase(new_name);
}
if (saved_reversed) {
q.patch_reversed.erase(new_name);
q.patch_reversed.insert(old_key);
}
if (renamed_pc_dir) {
rename_path(pc_patch_dir(q, new_name), pc_patch_dir(q, old_patch));
}
if (renamed_patch_file) {
rename_path(new_file, old_file);
}
return 1;
}
q.series = std::move(new_series);
if (q.is_applied(old_patch)) {
q.applied = std::move(new_applied);
}
out("Patch "); out(patch_path_display(q, old_patch));
out(" renamed to "); out_line(patch_path_display(q, new_name));
return 0;
}
int cmd_import(QuiltState &q, int argc, char **argv) {
int strip_level = -1;
std::string target_name;
bool force = false;
char dup_mode = 0; // o=overwrite, a=append, n=next
bool reversed = false;
std::vector<std::string> patchfiles;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-p" && i + 1 < argc) {
strip_level = checked_cast<int>(parse_int(argv[++i]));
} else if (arg == "-R") {
reversed = true;
} else if (arg == "-P" && i + 1 < argc) {
target_name = strip_patches_prefix(q, argv[++i]);
} else if (arg == "-f") {
force = true;
} else if (arg == "-d" && i + 1 < argc) {
dup_mode = argv[++i][0];
} else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
} else {
patchfiles.emplace_back(arg);
}
}
if (patchfiles.empty()) {
err_line("Usage: quilt import [-p num] [-R] [-P patch] [-f] [-d {o|a|n}] patchfile ...");
return 1;
}
if (!target_name.empty() && patchfiles.size() > 1) {
err_line("Option `-P' can only be used when importing a single patch");
return 1;
}
if (!ensure_pc_dir(q)) {
return 1;
}
// Ensure patches dir exists
std::string patches_abs = path_join(q.work_dir, q.patches_dir);
if (!is_directory(patches_abs)) {
if (!make_dirs(patches_abs)) {
err_line("Failed to create " + patches_abs);
return 1;
}
}
for (const auto &patchfile : patchfiles) {
// Determine target name
std::string name;
if (!target_name.empty()) {
name = target_name;
} else {
name = basename(patchfile);
}
std::string dest = path_join(q.work_dir, q.patches_dir, name);
// Check if target exists in series
auto existing = q.find_in_series(name);
if (existing && q.is_applied(name)) {
err_line("Patch " + patch_path_display(q, name) +
" is applied");
return 1;
}
if (existing && !force) {
err_line("Patch " + patch_path_display(q, name) +
" exists. Replace with -f.");
return 1;
}
// Ensure parent directory of dest exists (for subdir patch names)
std::string dest_dir = dirname(dest);
if (!is_directory(dest_dir)) {
if (!make_dirs(dest_dir)) {
err_line("Failed to create " + dest_dir);
return 1;
}
}
// Copy patchfile to patches/<name>, handling -d header mode
if (existing && force && dup_mode && dup_mode != 'n') {
// Merge headers based on -d mode
std::string old_content = read_file(dest);
std::string new_content = read_file(patchfile);
std::string old_hdr = extract_header(old_content);
std::string new_hdr = extract_header(new_content);
std::string merged_header;
if (dup_mode == 'o') {
merged_header = old_hdr;
} else if (dup_mode == 'a') {
merged_header = old_hdr;
if (!merged_header.empty() && merged_header.back() != '\n')
merged_header += '\n';
merged_header += "---\n";
merged_header += new_hdr;
}
std::string result = replace_header(new_content, merged_header);
if (!write_file(dest, result)) {
err_line("Failed to write " + dest);
return 1;
}
} else if (existing && force && !dup_mode) {
// Both patches exist and no -d flag: check if both have headers
std::string old_content = read_file(dest);
std::string new_content = read_file(patchfile);
std::string old_hdr = extract_header(old_content);
std::string new_hdr = extract_header(new_content);
if (!old_hdr.empty() && !new_hdr.empty() && old_hdr != new_hdr) {
err_line("Patch headers differ:");
err_line("@@ -1 +1 @@");
err_line("-" + old_hdr);
err_line("+" + new_hdr);
err_line("Please use -d {o|a|n} to specify which patch "
"header(s) to keep.");
return 1;
}
if (!copy_file(patchfile, dest)) {
err_line("Failed to copy " + patchfile + " to " + dest);
return 1;
}
} else {
if (!copy_file(patchfile, dest)) {
err_line("Failed to copy " + patchfile + " to " + dest);
return 1;
}
}
// Update per-patch metadata
if (strip_level >= 0 && strip_level != 1) {
q.patch_strip_level[name] = strip_level;
} else if (strip_level < 0) {
q.patch_strip_level.erase(name);
}
if (reversed) {
q.patch_reversed.insert(name);
} else {
q.patch_reversed.erase(name);
}
// Add to series if not already present
if (!existing) {
// Insert after top applied patch, or at end if none applied
ptrdiff_t top_idx = q.top_index();
auto new_series = q.series;
if (top_idx >= 0 && top_idx + 1 < std::ssize(new_series)) {
new_series.insert(new_series.begin() + top_idx + 1, name);
} else {
new_series.push_back(name);
}
if (!write_series_checked(q, new_series)) {
delete_file(dest);
return 1;
}
q.series = std::move(new_series);
} else {
// Overwriting existing patch — rewrite series for metadata update
if (!write_series_checked(q, q.series)) {
return 1;
}
}
if (existing && force) {
out_line("Replacing patch " + patch_path_display(q, name) +
" with new version");
} else {
out_line("Importing patch " + patchfile +
" (stored as " + patch_path_display(q, name) + ")");
}
}
return 0;
}
// Remove an existing diffstat section from a header.
// Detects "---" separator followed by " file | N ++--" lines ending
// with a "N file(s) changed" summary line.
static std::string strip_diffstat(std::string_view header) {
auto lines = split_lines(header);
std::string result;
for (ptrdiff_t i = 0; i < std::ssize(lines); ++i) {
const auto &line = lines[checked_cast<size_t>(i)];
// Detect "---" separator followed by diffstat, or bare diffstat
ptrdiff_t ds_start = i;
if (line == "---" && i + 1 < std::ssize(lines)) {
ds_start = i + 1;
}
const auto &first = lines[checked_cast<size_t>(ds_start)];
if (!first.empty() && first[0] == ' ' &&
str_find(first, '|') >= 0) {
bool found_summary = false;
ptrdiff_t summary_end = -1;
for (ptrdiff_t j = ds_start; j < std::ssize(lines); ++j) {
const auto &l = lines[checked_cast<size_t>(j)];
if (l.find("changed") != std::string::npos &&
l.find("file") != std::string::npos) {
found_summary = true;
summary_end = j;
break;
}
if (l.empty() || (l[0] != ' ' && str_find(l, '|') < 0))
break;
}
if (found_summary) {
// Keep the "---" separator if the diffstat followed it
if (ds_start != i) {
result += line;
result += '\n';
}
i = summary_end;
if (i + 1 < std::ssize(lines) && lines[checked_cast<size_t>(i + 1)].empty())
i++;
continue;
}
}
result += line;
result += '\n';
}
return result;
}
// Strip trailing whitespace from each line of a header.
static std::string strip_header_trailing_ws(std::string_view header) {
std::string result;
auto lines = split_lines(header);
for (const auto &line : lines) {
if (line.empty()) {
result += '\n';
continue;
}
// Strip \r from CRLF before checking for trailing whitespace
std::string_view l = line;
if (!l.empty() && l.back() == '\r') l.remove_suffix(1);
auto end = l.find_last_not_of(" \t");
if (end == std::string::npos) {
result += '\n';
} else {
result += l.substr(0, end + 1);
result += '\n';
}
}
return result;
}
static constexpr const char *dep3_template =
"Description: <short summary>\n"
" <long description that can span multiple lines>\n"
"Author: \n"
"Origin: <upstream|backport|vendor|other>, <URL>\n"
"Bug: <URL to upstream bug report>\n"
"Bug-Debian: https://bugs.debian.org/<bugnumber>\n"
"Forwarded: <URL|no|not-needed>\n"
"Applied-Upstream: <version|URL|commit>\n"
"Last-Update: <YYYY-MM-DD>\n";
int cmd_header(QuiltState &q, int argc, char **argv) {
enum Mode { PRINT, APPEND, REPLACE, EDIT };
Mode mode = PRINT;
bool opt_backup = false;
bool opt_dep3 = false;
bool opt_strip_ds = false;
bool opt_strip_ws = false;
std::string_view patch_arg;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-a") {
mode = APPEND;
} else if (arg == "-r") {
mode = REPLACE;
} else if (arg == "-e") {
mode = EDIT;
} else if (arg == "--backup") {
opt_backup = true;
} else if (arg == "--dep3") {
opt_dep3 = true;
} else if (arg == "--strip-diffstat") {
opt_strip_ds = true;
} else if (arg == "--strip-trailing-whitespace") {
opt_strip_ws = true;
} else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
} else {
patch_arg = strip_patches_prefix(q, arg);
}
}
// Determine patch
std::string_view patch;
if (!patch_arg.empty()) {
patch = patch_arg;
// Verify patch is in series
if (!q.find_in_series(patch)) {
err("Patch "); err(patch);
err_line(" is not in series");
return 1;
}
} else if (!q.applied.empty()) {
patch = q.applied.back();
} else {
err_line("No patches applied");
return 1;
}
std::string patch_file = path_join(q.work_dir, q.patches_dir, patch);
std::string content = read_file(patch_file);
// Helper to apply --strip-diffstat and --strip-trailing-whitespace
auto apply_strip = [&](std::string h) {
if (opt_strip_ds) h = strip_diffstat(h);
if (opt_strip_ws) h = strip_header_trailing_ws(h);
return h;
};
if (mode == PRINT) {
std::string header = apply_strip(extract_header(content));
out(header);
return 0;
}
if (mode == APPEND) {
std::string stdin_data = read_stdin();
std::string old_header = extract_header(content);
std::string new_header = apply_strip(old_header + stdin_data);
if (opt_backup) {
copy_file(patch_file, patch_file + "~");
}
std::string new_content = replace_header(content, new_header);
write_file(patch_file, new_content);
out_line("Appended text to header of patch " +
patch_path_display(q, patch));
return 0;
}
if (mode == REPLACE) {
std::string stdin_data = read_stdin();
std::string new_header = apply_strip(stdin_data);
if (opt_backup) {
copy_file(patch_file, patch_file + "~");
}
std::string new_content = replace_header(content, new_header);
write_file(patch_file, new_content);
out_line("Replaced header of patch " +
patch_path_display(q, patch));
return 0;
}
if (mode == EDIT) {
std::string editor = get_env("EDITOR");
if (editor.empty()) editor = "vi";
std::string header = extract_header(content);
// Insert DEP-3 template if header is empty and --dep3 given
if (opt_dep3 && trim(header).empty()) {
header = dep3_template;
}
std::string tmp_file = path_join(q.work_dir, ".pc/.quilt_header_tmp");
write_file(tmp_file, header);
int rc = run_cmd_tty({editor, tmp_file});
if (rc != 0) {
delete_file(tmp_file);
err_line("Editor exited with error");
return 1;
}
std::string new_header = apply_strip(read_file(tmp_file));
delete_file(tmp_file);
if (opt_backup) {
copy_file(patch_file, patch_file + "~");
}
std::string new_content = replace_header(content, new_header);
write_file(patch_file, new_content);
out_line("Replaced header of patch " + patch_path_display(q, patch));
return 0;
}
return 0;
}
int cmd_files(QuiltState &q, int argc, char **argv) {
bool opt_verbose = false;
bool opt_all = false;
bool opt_labels = false;
std::string combine_patch;
std::string_view patch_arg;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-v") {
opt_verbose = true;
} else if (arg == "-a") {
opt_all = true;
} else if (arg == "-l") {
opt_labels = true;
} else if (arg == "--combine" && i + 1 < argc) {
combine_patch = argv[++i];
} else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
} else {
patch_arg = strip_patches_prefix(q, arg);
}
}
// Determine target patch (topmost or specified)
std::string target_patch;
if (!patch_arg.empty()) {
target_patch = patch_arg;
} else if (!q.applied.empty()) {
target_patch = q.applied.back();
} else if (!opt_all) {
err_line("No patches applied");
return 1;
}
// Build list of patches to show files for
std::vector<std::string> patches_to_show;
if (opt_all) {
patches_to_show = q.applied;
} else if (!combine_patch.empty()) {
// Range from combine_patch through target_patch
std::string start = combine_patch;
if (start == "-") {
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
start = q.applied.front();
} else {
start = strip_patches_prefix(q, start);
}
bool in_range = false;
for (const auto &a : q.applied) {
if (a == start) in_range = true;
if (in_range) patches_to_show.push_back(a);
if (a == target_patch) break;
}
if (!in_range || patches_to_show.empty()) {
err("Patch ");
err(start);
err_line(" not applied");
return 1;
}
} else {
patches_to_show.push_back(target_patch);
}
// With labels (-l): iterate patches, output per-patch file listings
if (opt_labels) {
for (const auto &patch : patches_to_show) {
std::vector<std::string> file_list;
if (q.is_applied(patch)) {
file_list = files_in_patch(q, patch);
} else {
std::string patch_file = path_join(q.work_dir, q.patches_dir, patch);
std::string content = read_file(patch_file);
file_list = parse_patch_files(content);
}
std::ranges::sort(file_list);
for (const auto &f : file_list) {
out_line(patch + " " + f);
}
}
} else {
// Collect all files across patches
std::vector<std::string> all_files;
for (const auto &patch : patches_to_show) {
std::vector<std::string> file_list;
if (q.is_applied(patch)) {
file_list = files_in_patch(q, patch);
} else {
std::string patch_file = path_join(q.work_dir, q.patches_dir, patch);
std::string content = read_file(patch_file);
file_list = parse_patch_files(content);
}
for (auto &f : file_list) {
all_files.push_back(std::move(f));
}
}
std::ranges::sort(all_files);
for (const auto &f : all_files) {
if (opt_verbose) {
out_line(" " + f);
} else {
out_line(f);
}
}
}
return 0;
}
int cmd_patches(QuiltState &q, int argc, char **argv) {
bool opt_verbose = false;
std::vector<std::string> target_files;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-v") {
opt_verbose = true;
} else if (arg == "--color" || arg.starts_with("--color=")) {
if (arg.starts_with("--color=")) {
auto val = arg.substr(8);
if (val != "always" && val != "auto" && val != "never") {
err("Invalid --color value: "); err_line(val);
return 1;
}
}
} else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
} else {
target_files.push_back(subdir_path(q, arg));
}
}
if (target_files.empty()) {
err_line("Usage: quilt patches [-v] [--color] file [files...]");
return 1;
}
for (const auto &patch : q.series) {
bool touches = false;
if (q.is_applied(patch)) {
// Check .pc/<patch>/<file>
std::string pc_dir = pc_patch_dir(q, patch);
for (const auto &tf : target_files) {
std::string check = path_join(pc_dir, tf);
if (file_exists(check)) {
touches = true;
break;
}
}
} else {
// Parse patch file for references
std::string patch_file = path_join(q.work_dir, q.patches_dir, patch);
std::string content = read_file(patch_file);
auto patched_files = parse_patch_files(content);
for (const auto &tf : target_files) {
for (const auto &pf : patched_files) {
if (pf == tf) {
touches = true;
break;
}
}
if (touches) break;
}
}
if (touches) {
std::string display = patch;
if (opt_verbose) {
// Show applied status: = for top, + for other applied, space for unapplied
if (!q.applied.empty() && patch == q.applied.back()) {
out_line("= " + display);
} else if (q.is_applied(patch)) {
out_line("+ " + display);
} else {
out_line(" " + display);
}
} else {
out_line(display);
}
}
}
return 0;
}
int cmd_fold(QuiltState &q, int argc, char **argv) {
bool opt_reverse = false;
bool opt_quiet = false;
bool opt_force = false;
int strip_level = 1;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-R") {
opt_reverse = true;
} else if (arg == "-q") {
opt_quiet = true;
} else if (arg == "-f") {
opt_force = true;
} else if (arg == "-p" && i + 1 < argc) {
strip_level = checked_cast<int>(parse_int(argv[++i]));
} else if (arg.starts_with("-p") && arg.size() > 2 &&
arg[2] >= '0' && arg[2] <= '9') {
strip_level = checked_cast<int>(parse_int(arg.substr(2)));
} else if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
}
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
std::string top = q.applied.back();
std::string stdin_data = read_stdin();
if (stdin_data.empty()) {
return 0;
}
// Parse the incoming patch to find affected files
auto affected_files = parse_patch_files(stdin_data, strip_level);
// Track new files in the current patch
auto currently_tracked = files_in_patch(q, top);
for (const auto &f : affected_files) {
bool already_tracked = false;
for (const auto &t : currently_tracked) {
if (t == f) { already_tracked = true; break; }
}
if (!already_tracked) {
backup_file(q, top, f);
}
}
// Apply patch using built-in patch engine
PatchOptions patch_opts;
patch_opts.strip_level = strip_level;
patch_opts.reverse = opt_reverse;
patch_opts.force = opt_force;
patch_opts.quiet = opt_quiet;
auto extra_patch_opts = shell_split(get_env("QUILT_PATCH_OPTS"));
for (const auto &opt : extra_patch_opts) {
std::string_view o = opt;
if (o == "-R") patch_opts.reverse = true;
else if (o == "-f" || o == "--force") patch_opts.force = true;
else if (o == "-s") patch_opts.quiet = true;
else if (o == "-E") patch_opts.remove_empty = true;
else if (o.starts_with("--fuzz=")) {
patch_opts.fuzz = checked_cast<int>(parse_int(o.substr(7)));
}
}
PatchResult r = builtin_patch(stdin_data, patch_opts);
if (!opt_quiet && !r.out.empty()) {
out(r.out);
}
if (!r.err.empty()) err(r.err);
if (r.exit_code != 0 && !opt_force) {
return 1;
}
return 0;
}
int cmd_fork(QuiltState &q, int argc, char **argv) {
if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
std::string old_name = q.applied.back();
std::string new_name;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
new_name = strip_patches_prefix(q, arg);
break;
}
// Generate default name if none given: increment "-N" suffix before extension
if (new_name.empty()) {
auto dot = str_rfind(old_name, '.');
std::string base;
std::string ext;
if (dot > 0) {
base = old_name.substr(0, checked_cast<size_t>(dot));
ext = old_name.substr(checked_cast<size_t>(dot));
} else {
base = old_name;
}
// Check for existing -N suffix and increment it
auto dash = str_rfind(base, '-');
if (dash >= 0) {
std::string_view suffix = std::string_view(base).substr(
checked_cast<size_t>(dash) + 1);
int n = 0;
auto [ptr, ec] = std::from_chars(suffix.data(),
suffix.data() + suffix.size(), n);
if (ec == std::errc{} && ptr == suffix.data() + suffix.size()) {
new_name = base.substr(0, checked_cast<size_t>(dash) + 1)
+ std::to_string(n + 1) + ext;
} else {
new_name = base + "-2" + ext;
}
} else {
new_name = base + "-2" + ext;
}
}
// Check that the new name doesn't already exist in series
if (q.find_in_series(new_name)) {
err("Patch "); err(new_name); err_line(" already exists in series");
return 1;
}
auto idx = q.find_in_series(old_name);
if (!idx) {
err("Patch "); err(old_name); err_line(" is not in series");
return 1;
}
// Copy patch file
std::string old_file = path_join(q.work_dir, q.patches_dir, old_name);
std::string new_file = path_join(q.work_dir, q.patches_dir, new_name);
bool copied_patch_file = false;
if (file_exists(old_file)) {
std::string new_dir = dirname(new_file);
if (!is_directory(new_dir)) {
if (!make_dirs(new_dir)) {
err_line("Failed to create " + new_dir);
return 1;
}
}
if (!copy_file(old_file, new_file)) {
err_line("Failed to copy " + old_file + " to " + new_file);
return 1;
}
copied_patch_file = true;
}
// Rename .pc/ directory
std::string old_pc = pc_patch_dir(q, old_name);
std::string new_pc = pc_patch_dir(q, new_name);
bool renamed_pc_dir = false;
if (is_directory(old_pc)) {
if (!rename_path(old_pc, new_pc)) {
if (copied_patch_file) {
delete_file(new_file);
}
err_line("Failed to rename " + old_pc + " to " + new_pc);
return 1;
}
renamed_pc_dir = true;
}
// Migrate per-patch metadata before writing series
auto sl_it = q.patch_strip_level.find(old_name);
int saved_strip = -1;
bool saved_reversed = false;
if (sl_it != q.patch_strip_level.end()) {
saved_strip = sl_it->second;
q.patch_strip_level[new_name] = sl_it->second;
q.patch_strip_level.erase(sl_it);
}
if (q.patch_reversed.erase(old_name)) {
saved_reversed = true;
q.patch_reversed.insert(new_name);
}
auto new_series = q.series;
new_series[checked_cast<size_t>(*idx)] = new_name;
if (!write_series_checked(q, new_series)) {
if (saved_strip >= 0) {
q.patch_strip_level[old_name] = saved_strip;
q.patch_strip_level.erase(new_name);
}
if (saved_reversed) {
q.patch_reversed.erase(new_name);
q.patch_reversed.insert(old_name);
}
if (renamed_pc_dir) {
rename_path(new_pc, old_pc);
}
if (copied_patch_file) {
delete_file(new_file);
}
return 1;
}
auto new_applied = q.applied;
for (auto &a : new_applied) {
if (a == old_name) {
a = new_name;
break;
}
}
if (!write_applied_checked(q, new_applied)) {
write_series_checked(q, q.series);
if (saved_strip >= 0) {
q.patch_strip_level[old_name] = saved_strip;
q.patch_strip_level.erase(new_name);
}
if (saved_reversed) {
q.patch_reversed.erase(new_name);
q.patch_reversed.insert(old_name);
}
if (renamed_pc_dir) {
rename_path(new_pc, old_pc);
}
if (copied_patch_file) {
delete_file(new_file);
}
return 1;
}
q.series = std::move(new_series);
q.applied = std::move(new_applied);
out_line("Fork of patch " + old_name +
" created as " + new_name);
return 0;
}
int cmd_upgrade(QuiltState &, int argc, char **argv)
{
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "-h" || arg == "--help") {
out_line("Usage: quilt upgrade");
out_line("");
out_line("Upgrade the metadata in the .pc/ directory from version 1 to");
out_line("version 2. This command does nothing because quilt.cpp only");
out_line("supports the version 2 format.");
return 0;
}
if (arg[0] == '-') {
err("Unrecognized option: "); err_line(arg);
return 1;
}
}
return 0;
}
// === src/cmd_graph.cpp ===
// This is free and unencumbered software released into the public domain.
#include <cmath>
#include <iomanip>
#include <map>
#include <optional>
#include <regex>
#include <set>
#include <sstream>
namespace {
struct LineRanges {
bool computed = false;
std::vector<int> left;
std::vector<int> right;
};
struct GraphNode {
int number = 0;
std::string name;
std::map<std::string, LineRanges> files;
std::vector<std::string> attrs;
};
struct EdgeData {
std::vector<std::string> names;
};
using EdgeKey = std::pair<int, int>;
static bool is_number(std::string_view value) {
if (value.empty()) return false;
for (char c : value) {
if (c < '0' || c > '9') return false;
}
return true;
}
static bool is_zero_length_file(std::string_view path) {
return file_exists(path) && read_file(path).empty();
}
static std::string dot_escape(std::string_view text) {
std::string escaped;
escaped.reserve(checked_cast<size_t>(std::ssize(text)));
for (char c : text) {
if (c == '\\' || c == '"') {
escaped += '\\';
escaped += c;
} else if (c == '\n') {
escaped += "\\n";
} else {
escaped += c;
}
}
return escaped;
}
static int parse_hunk_count(const std::ssub_match &match) {
if (!match.matched || match.str().empty()) {
return 1;
}
return checked_cast<int>(parse_int(match.str()));
}
static LineRanges parse_ranges(std::string_view diff_text) {
static const std::regex hunk_regex(
R"(^@@ -([0-9]+)(?:,([0-9]+))? \+([0-9]+)(?:,([0-9]+))? @@)");
LineRanges ranges;
ranges.computed = true;
for (const auto &line : split_lines(diff_text)) {
std::smatch match;
if (!std::regex_search(line, match, hunk_regex)) continue;
int old_start = checked_cast<int>(parse_int(match[1].str()));
int old_count = parse_hunk_count(match[2]);
int new_start = checked_cast<int>(parse_int(match[3].str()));
int new_count = parse_hunk_count(match[4]);
ranges.left.push_back(new_start);
ranges.left.push_back(new_start + new_count);
ranges.right.push_back(old_start);
ranges.right.push_back(old_start + old_count);
}
return ranges;
}
static std::optional<int> next_node_for_file(const std::vector<GraphNode> &nodes,
int index,
std::string_view file) {
for (int i = index + 1; i < std::ssize(nodes); ++i) {
if (nodes[checked_cast<size_t>(i)].files.contains(std::string(file))) {
return i;
}
}
return std::nullopt;
}
static void compute_ranges(const QuiltState &q,
std::vector<GraphNode> &nodes,
int index,
std::string_view file,
int context_lines) {
auto it = nodes[checked_cast<size_t>(index)].files.find(std::string(file));
if (it == nodes[checked_cast<size_t>(index)].files.end() || it->second.computed) return;
LineRanges &ranges = it->second;
ranges.computed = true;
std::string old_path = path_join(pc_patch_dir(q, nodes[checked_cast<size_t>(index)].name), file);
std::string new_path;
auto next = next_node_for_file(nodes, index, file);
if (next.has_value()) {
new_path = path_join(pc_patch_dir(q, nodes[checked_cast<size_t>(*next)].name), file);
} else {
new_path = path_join(q.work_dir, file);
}
bool old_missing = is_zero_length_file(old_path);
bool new_missing = is_zero_length_file(new_path);
if (old_missing && new_missing) {
return;
}
DiffResult diff = builtin_diff(
old_missing ? "/dev/null" : std::string_view(old_path),
new_missing ? "/dev/null" : std::string_view(new_path),
context_lines);
if (diff.exit_code == 0) {
return;
}
ranges = parse_ranges(diff.output);
}
static bool is_conflict(const QuiltState &q,
std::vector<GraphNode> &nodes,
int from,
int to,
std::string_view file,
int context_lines) {
compute_ranges(q, nodes, from, file, context_lines);
compute_ranges(q, nodes, to, file, context_lines);
const auto file_key = std::string(file);
const auto &a = nodes[checked_cast<size_t>(from)].files[file_key].right;
const auto &b = nodes[checked_cast<size_t>(to)].files[file_key].left;
ptrdiff_t ia = 0;
ptrdiff_t ib = 0;
while (ia < std::ssize(a) && ib < std::ssize(b)) {
ptrdiff_t rem_a = std::ssize(a) - ia;
ptrdiff_t rem_b = std::ssize(b) - ib;
if (a[checked_cast<size_t>(ia)] < b[checked_cast<size_t>(ib)]) {
if ((rem_b % 2) == 1) return true;
++ia;
} else if (a[checked_cast<size_t>(ia)] > b[checked_cast<size_t>(ib)]) {
if ((rem_a % 2) == 1) return true;
++ib;
} else {
if ((rem_a % 2) == (rem_b % 2)) return true;
++ia;
++ib;
}
}
return false;
}
static void add_edge(std::map<EdgeKey, EdgeData> &edges,
int earlier,
int later,
std::string_view file) {
auto &edge = edges[{earlier, later}];
edge.names.emplace_back(file);
}
static std::set<int> collect_reachable(const std::map<EdgeKey, EdgeData> &edges,
int start,
bool forward) {
std::map<int, std::vector<int>> adjacency;
for (const auto &[key, value] : edges) {
(void)value;
int from = key.first;
int to = key.second;
if (forward) {
adjacency[from].push_back(to);
} else {
adjacency[to].push_back(from);
}
}
std::set<int> seen;
std::vector<int> stack = {start};
while (!stack.empty()) {
int node = stack.back();
stack.pop_back();
if (!seen.insert(node).second) continue;
auto it = adjacency.find(node);
if (it == adjacency.end()) continue;
for (int next : it->second) {
stack.push_back(next);
}
}
return seen;
}
static bool has_alternate_path(int from,
int to,
const std::map<int, std::vector<int>> &adjacency,
EdgeKey skip_edge) {
std::vector<int> stack = {from};
std::set<int> seen;
while (!stack.empty()) {
int node = stack.back();
stack.pop_back();
if (!seen.insert(node).second) continue;
auto it = adjacency.find(node);
if (it == adjacency.end()) continue;
for (int next : it->second) {
if (EdgeKey{node, next} == skip_edge) continue;
if (next == to) return true;
stack.push_back(next);
}
}
return false;
}
static void reduce_edges(std::map<EdgeKey, EdgeData> &edges) {
std::map<int, std::vector<int>> adjacency;
for (const auto &[key, value] : edges) {
(void)value;
adjacency[key.first].push_back(key.second);
}
std::vector<EdgeKey> to_remove;
for (const auto &[key, value] : edges) {
(void)value;
if (has_alternate_path(key.first, key.second, adjacency, key)) {
to_remove.push_back(key);
}
}
for (const auto &key : to_remove) {
edges.erase(key);
}
}
static std::string format_len_attr(int from, int to) {
std::ostringstream value;
value << std::fixed << std::setprecision(2)
<< std::log(static_cast<double>(std::abs(to - from) + 3));
return "len=\"" + value.str() + "\"";
}
static std::string render_dot(const std::vector<GraphNode> &nodes,
std::map<EdgeKey, EdgeData> edges,
std::set<int> used_nodes,
bool reduce,
bool edge_labels) {
if (reduce) {
reduce_edges(edges);
used_nodes.clear();
for (const auto &[key, value] : edges) {
(void)value;
used_nodes.insert(key.first);
used_nodes.insert(key.second);
}
// Preserve the selected node (style=bold) even if it has no edges
for (const auto &node : nodes) {
for (const auto &attr : node.attrs) {
if (attr == "style=bold") {
used_nodes.insert(node.number);
break;
}
}
}
}
std::string dot = "digraph dependencies {\n";
for (const auto &node : nodes) {
if (!used_nodes.contains(node.number)) continue;
std::vector<std::string> attrs = node.attrs;
attrs.push_back("label=\"" + dot_escape(node.name) + "\"");
dot += "\tn" + std::to_string(node.number);
if (!attrs.empty()) {
dot += " [";
for (ptrdiff_t i = 0; i < std::ssize(attrs); ++i) {
if (i != 0) dot += ",";
dot += attrs[checked_cast<size_t>(i)];
}
dot += "]";
}
dot += ";\n";
}
for (auto &[key, edge] : edges) {
std::ranges::sort(edge.names);
auto [first, last] = std::ranges::unique(edge.names);
edge.names.erase(first, last);
std::vector<std::string> attrs;
if (edge_labels && !edge.names.empty()) {
std::string label;
for (ptrdiff_t i = 0; i < std::ssize(edge.names); ++i) {
if (i != 0) label += "\\n";
label += dot_escape(edge.names[checked_cast<size_t>(i)]);
}
attrs.push_back("label=\"" + label + "\"");
}
attrs.push_back(format_len_attr(key.first, key.second));
dot += "\tn" + std::to_string(key.first) + " -> n" + std::to_string(key.second);
dot += " [";
for (ptrdiff_t i = 0; i < std::ssize(attrs); ++i) {
if (i != 0) dot += ",";
dot += attrs[checked_cast<size_t>(i)];
}
dot += "];\n";
}
dot += "}\n";
return dot;
}
} // namespace
int cmd_graph(QuiltState &q, int argc, char **argv) {
bool opt_all = false;
bool opt_reduce = false;
bool opt_edge_labels = false;
std::optional<int> opt_lines;
std::string_view patch_arg;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "--all") {
opt_all = true;
} else if (arg == "--reduce") {
opt_reduce = true;
} else if (arg == "--lines") {
opt_lines = 2;
if (i + 1 < argc && is_number(argv[i + 1])) {
opt_lines = checked_cast<int>(parse_int(argv[++i]));
}
} else if (arg.starts_with("--lines=")) {
std::string value(arg.substr(8));
if (!is_number(value)) {
err_line("Usage: quilt graph [--all] [--reduce] [--lines[=num]] [--edge-labels=files] [-T ps] [patch]");
return 1;
}
opt_lines = checked_cast<int>(parse_int(value));
} else if (arg == "--edge-labels") {
if (i + 1 >= argc || std::string_view(argv[i + 1]) != "files") {
err_line("Usage: quilt graph [--all] [--reduce] [--lines[=num]] [--edge-labels=files] [-T ps] [patch]");
return 1;
}
opt_edge_labels = true;
++i;
} else if (arg == "--edge-labels=files") {
opt_edge_labels = true;
} else if (arg == "-T") {
if (i + 1 >= argc || std::string_view(argv[i + 1]) != "ps") {
err_line("Usage: quilt graph [--all] [--reduce] [--lines[=num]] [--edge-labels=files] [-T ps] [patch]");
return 1;
}
++i;
err_line("quilt graph -T ps: not implemented");
return 1;
} else if (arg == "-Tps") {
err_line("quilt graph -T ps: not implemented");
return 1;
} else if (!arg.empty() && arg[0] == '-') {
err_line("Usage: quilt graph [--all] [--reduce] [--lines[=num]] [--edge-labels=files] [-T ps] [patch]");
return 1;
} else if (!patch_arg.empty()) {
err_line("Usage: quilt graph [--all] [--reduce] [--lines[=num]] [--edge-labels=files] [-T ps] [patch]");
return 1;
} else {
patch_arg = strip_patches_prefix(q, arg);
}
}
if (!patch_arg.empty() && opt_all) {
err_line("Usage: quilt graph [--all] [--reduce] [--lines[=num]] [--edge-labels=files] [-T ps] [patch]");
return 1;
}
std::string_view selected_patch;
if (!opt_all) {
if (q.applied.empty()) {
if (!q.series_file_exists) {
err_line("No series file found");
} else if (q.series.empty()) {
err_line("No patches in series");
} else {
err_line("No patches applied");
}
return 1;
}
selected_patch = patch_arg.empty() ? std::string_view(q.applied.back()) : patch_arg;
if (!q.find_in_series(selected_patch).has_value()) {
err("Patch "); err(selected_patch); err_line(" is not in series");
return 1;
}
if (!q.is_applied(selected_patch)) {
err("Patch "); err(selected_patch); err_line(" is not applied");
return 1;
}
} else if (q.applied.empty()) {
err_line("No patches applied");
return 1;
}
std::vector<GraphNode> nodes;
nodes.reserve(checked_cast<size_t>(std::ssize(q.applied)));
for (ptrdiff_t i = 0; i < std::ssize(q.applied); ++i) {
const std::string &patch = q.applied[checked_cast<size_t>(i)];
auto files = files_in_patch(q, patch);
std::ranges::sort(files);
GraphNode node;
node.number = checked_cast<int>(i);
node.name = patch;
for (const auto &file : files) {
node.files.emplace(file, LineRanges{});
}
nodes.push_back(std::move(node));
}
std::set<int> used_nodes;
if (!selected_patch.empty()) {
auto selected = std::ranges::find_if(nodes,
[&](const GraphNode &node) {
return node.name == selected_patch;
});
if (selected == nodes.end()) {
err("Patch "); err(selected_patch); err_line(" is not applied");
return 1;
}
selected->attrs.push_back("style=bold");
selected->attrs.push_back("color=grey");
std::set<std::string> selected_files;
for (const auto &[file, ranges] : selected->files) {
(void)ranges;
selected_files.insert(file);
}
for (auto &node : nodes) {
for (auto it = node.files.begin(); it != node.files.end(); ) {
if (!selected_files.contains(it->first)) {
it = node.files.erase(it);
} else {
++it;
}
}
}
}
std::map<std::string, std::vector<int>> files_seen;
std::map<EdgeKey, EdgeData> edges;
for (auto &node : nodes) {
for (const auto &[file, ranges] : node.files) {
(void)ranges;
auto seen_it = files_seen.find(file);
if (seen_it != files_seen.end()) {
std::optional<int> dependency;
if (opt_lines.has_value()) {
for (auto prev = seen_it->second.rbegin();
prev != seen_it->second.rend();
++prev) {
if (is_conflict(q, nodes, node.number, *prev, file, *opt_lines)) {
dependency = *prev;
break;
}
}
} else {
dependency = seen_it->second.back();
}
if (dependency.has_value()) {
add_edge(edges, *dependency, node.number, file);
used_nodes.insert(*dependency);
used_nodes.insert(node.number);
}
}
files_seen[file].push_back(node.number);
}
}
if (!selected_patch.empty()) {
int selected_index = -1;
for (const auto &node : nodes) {
if (node.name == selected_patch) {
selected_index = node.number;
break;
}
}
std::set<int> reachable = collect_reachable(edges, selected_index, true);
std::set<int> reverse = collect_reachable(edges, selected_index, false);
reachable.insert(reverse.begin(), reverse.end());
for (auto it = edges.begin(); it != edges.end(); ) {
if (!reachable.contains(it->first.first) ||
!reachable.contains(it->first.second)) {
it = edges.erase(it);
} else {
++it;
}
}
used_nodes = std::move(reachable);
}
std::string dot = render_dot(nodes, edges, used_nodes, opt_reduce, opt_edge_labels);
out(dot);
return 0;
}
// === src/cmd_mail.cpp ===
// This is free and unencumbered software released into the public domain.
#include <cstdio>
static std::string extract_diff(std::string_view content) {
auto lines = split_lines(content);
std::string diff;
bool in_diff = false;
for (const auto &line : lines) {
if (!in_diff) {
if (line.starts_with("Index:") ||
line.starts_with("--- ") ||
line.starts_with("diff ") ||
line.starts_with("===")) {
in_diff = true;
}
}
if (in_diff) {
diff += line;
diff += '\n';
}
}
return diff;
}
static bool has_non_ascii(std::string_view s) {
for (char ch : s) {
if (static_cast<unsigned char>(ch) > 127) return true;
}
return false;
}
// RFC 2047 quoted-printable encoding for a header value
static std::string rfc2047_encode(std::string_view s) {
// Encode as =?UTF-8?q?...?=
// Characters that must be encoded: non-ASCII, =, ?, _, space
std::string result = "=?UTF-8?q?";
ptrdiff_t line_len = 10; // length of "=?UTF-8?q?"
for (char ch : s) {
auto c = static_cast<unsigned char>(ch);
std::string encoded;
if (c == '=' || c == '?' || c == '_' || c == ' ' || c > 127) {
static constexpr char hex[] = "0123456789ABCDEF";
encoded = {'=', hex[c >> 4], hex[c & 0xf]};
} else {
encoded = std::string(1, ch);
}
// Line wrap: if adding this would exceed ~75 chars, close and start new encoded word
if (line_len + std::ssize(encoded) + 2 > 75) { // 2 for "?="
result += "?=\n =?UTF-8?q?";
line_len = 12; // " =?UTF-8?q?"
}
result += encoded;
line_len += std::ssize(encoded);
}
result += "?=";
return result;
}
// Format RFC 2822 date
static std::string format_rfc2822_date(int64_t t) {
DateTime dt = local_time(t);
int tz_hours = dt.utc_offset / 3600;
int tz_mins = (dt.utc_offset % 3600) / 60;
if (tz_mins < 0) tz_mins = -tz_mins;
static constexpr const char *days[] =
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
static constexpr const char *months[] =
{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
return std::format("{}, {} {} {} {:02d}:{:02d}:{:02d} {:+03d}{:02d}",
days[dt.weekday],
dt.day,
months[dt.month - 1],
dt.year,
dt.hour, dt.min, dt.sec,
tz_hours, tz_mins);
}
// FNV-1a 64-bit hash
static uint64_t fnv1a_64(std::string_view data) {
uint64_t h = 0xcbf29ce484222325ULL;
for (char ch : data)
h = (h ^ static_cast<uint64_t>(static_cast<unsigned char>(ch))) * 0x100000001b3ULL;
return h;
}
// Generate a Message-ID
static std::string make_message_id(int64_t t, int seq,
std::string_view from,
std::string_view content) {
// Extract domain from the from address
std::string domain = "localhost";
auto at = str_find(from, '@');
if (at >= 0) {
auto end = str_find(from, '>', at);
if (end < 0) end = std::ssize(from);
domain = std::string(from.substr(checked_cast<size_t>(at + 1), checked_cast<size_t>(end - at - 1)));
}
uint64_t h = fnv1a_64(content);
return std::format("<{}.{}.{:016x}.{}@{}>", t, seq, h, current_time(), domain);
}
// Compute the width needed for zero-padded patch numbers
static int num_width(int n) {
if (n < 10) return 1;
if (n < 100) return 2;
if (n < 1000) return 3;
return 4;
}
int cmd_mail(QuiltState &q, int argc, char **argv) {
std::string mbox_file;
std::string from_addr;
std::string sender_addr;
std::string prefix = "PATCH";
std::vector<std::string> to_addrs;
std::vector<std::string> cc_addrs;
std::vector<std::string> bcc_addrs;
std::vector<std::string> positional;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "--mbox" && i + 1 < argc) {
mbox_file = argv[++i];
} else if (arg == "--send") {
err_line("quilt mail: send mode is not supported; use --mbox");
return 1;
} else if (arg == "--sender" && i + 1 < argc) {
sender_addr = argv[++i];
} else if (arg == "--from" && i + 1 < argc) {
from_addr = argv[++i];
} else if (arg == "--prefix" && i + 1 < argc) {
prefix = argv[++i];
} else if (arg == "--to" && i + 1 < argc) {
to_addrs.emplace_back(argv[++i]);
} else if (arg == "--cc" && i + 1 < argc) {
cc_addrs.emplace_back(argv[++i]);
} else if (arg == "--bcc" && i + 1 < argc) {
bcc_addrs.emplace_back(argv[++i]);
} else if (arg == "--subject" && i + 1 < argc) {
++i; // consume and ignore (cover letter not generated)
} else if (arg == "-m" && i + 1 < argc) {
++i; // consume and ignore (cover letter not generated)
} else if (arg == "-M" && i + 1 < argc) {
++i; // consume and ignore (cover letter not generated)
} else if (arg == "--reply-to" && i + 1 < argc) {
++i; // consume and ignore (cover letter not generated)
} else if (arg == "--charset" && i + 1 < argc) {
++i; // consume and ignore
} else if (arg == "--signature" && i + 1 < argc) {
++i; // consume and ignore
} else if (arg == "-h" || arg == "--help") {
out_line("Usage: quilt mail {--mbox file} [--prefix prefix] "
"[--sender ...] [--from ...] [--to ...] [--cc ...] "
"[--bcc ...] [first_patch [last_patch]]");
return 0;
} else if (arg[0] != '-' || arg == "-") {
positional.emplace_back(arg);
} else {
err("quilt mail: unknown option: ");
err_line(arg);
return 1;
}
}
if (mbox_file.empty()) {
err_line("quilt mail: --mbox is required");
return 1;
}
// Determine From address
std::string effective_from = from_addr;
if (effective_from.empty()) {
effective_from = sender_addr;
}
if (effective_from.empty()) {
err_line("quilt mail: --from or --sender is required");
return 1;
}
if (q.series.empty()) {
err_line("No patches in series");
return 1;
}
// Resolve patch range
ptrdiff_t first_idx = 0;
ptrdiff_t last_idx = std::ssize(q.series) - 1;
if (std::ssize(positional) == 1) {
// Single patch
std::string name = positional[0];
if (name == "-") {
// "-" as single arg means all patches
} else {
auto idx = q.find_in_series(name);
if (!idx) {
err_line("Patch " + name + " is not in series");
return 1;
}
first_idx = *idx;
last_idx = *idx;
}
} else if (std::ssize(positional) == 2) {
std::string first_name = positional[0];
std::string last_name = positional[1];
if (first_name == "-") {
first_idx = 0;
} else {
auto idx = q.find_in_series(first_name);
if (!idx) {
err_line("Patch " + first_name + " is not in series");
return 1;
}
first_idx = *idx;
}
if (last_name == "-") {
last_idx = std::ssize(q.series) - 1;
} else {
auto idx = q.find_in_series(last_name);
if (!idx) {
err_line("Patch " + last_name + " is not in series");
return 1;
}
last_idx = *idx;
}
if (first_idx > last_idx) {
err_line("quilt mail: first patch must come before last patch in series");
return 1;
}
} else if (std::ssize(positional) > 2) {
err_line("Usage: quilt mail {--mbox file} [options] [first_patch [last_patch]]");
return 1;
}
ptrdiff_t total = last_idx - first_idx + 1;
int width = num_width(checked_cast<int>(total));
std::string mbox;
for (ptrdiff_t i = first_idx; i <= last_idx; ++i) {
const std::string &patch = q.series[checked_cast<size_t>(i)];
std::string patch_file = path_join(q.work_dir, q.patches_dir, patch);
std::string content = read_file(patch_file);
if (content.empty()) {
err("Warning: patch ");
err(patch);
err_line(" is empty, skipping");
continue;
}
// Extract header and diff
std::string header = extract_header(content);
std::string diff = extract_diff(content);
// Split header into subject (first line) and body (rest)
std::string subject_text;
std::string body;
if (!header.empty()) {
auto hdr_lines = split_lines(header);
// Find first non-empty line for subject
ptrdiff_t subj_line = 0;
while (subj_line < std::ssize(hdr_lines) && trim(hdr_lines[checked_cast<size_t>(subj_line)]).empty()) {
subj_line++;
}
if (subj_line < std::ssize(hdr_lines)) {
subject_text = trim(hdr_lines[checked_cast<size_t>(subj_line)]);
// Remaining lines become body
for (ptrdiff_t j = subj_line + 1; j < std::ssize(hdr_lines); ++j) {
body += hdr_lines[checked_cast<size_t>(j)];
body += '\n';
}
}
}
// If no header at all, use patch name as subject
if (subject_text.empty()) {
subject_text = patch;
}
// Build Subject with prefix
std::string subject_prefix;
if (total == 1) {
subject_prefix = "[" + prefix + "]";
} else {
int seq = checked_cast<int>(i - first_idx + 1);
subject_prefix = std::format("[{} {:0{}d}/{}]",
prefix, seq, width, checked_cast<int>(total));
}
std::string full_subject = subject_prefix + " " + subject_text;
// RFC 2047 encode subject if needed
std::string subject_header;
if (has_non_ascii(full_subject)) {
subject_header = "Subject: " + rfc2047_encode(full_subject);
} else {
subject_header = "Subject: " + full_subject;
}
int64_t msg_time = file_mtime(patch_file);
if (msg_time == -1) {
msg_time = current_time();
}
int seq = checked_cast<int>(i - first_idx + 1);
// Build message
std::string msg;
// Mbox separator
msg += "From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001\n";
// From header
msg += "From: " + effective_from + "\n";
// Date header
msg += "Date: " + format_rfc2822_date(msg_time) + "\n";
// Subject header
msg += subject_header + "\n";
// Message-ID
msg += "Message-ID: " + make_message_id(msg_time, seq, effective_from, content) + "\n";
// MIME headers if non-ASCII in body
if (has_non_ascii(header) || has_non_ascii(diff) || has_non_ascii(full_subject)) {
msg += "MIME-Version: 1.0\n";
msg += "Content-Type: text/plain; charset=UTF-8\n";
msg += "Content-Transfer-Encoding: 8bit\n";
}
// Optional To/Cc/Bcc
for (const auto &addr : to_addrs) {
msg += "To: " + addr + "\n";
}
for (const auto &addr : cc_addrs) {
msg += "Cc: " + addr + "\n";
}
for (const auto &addr : bcc_addrs) {
msg += "Bcc: " + addr + "\n";
}
// Blank line separating headers from body
msg += "\n";
// Body (remaining header text)
if (!body.empty()) {
// Trim leading blank lines from body
std::string_view bv = body;
while (bv.starts_with("\n")) {
bv = bv.substr(1);
}
if (!bv.empty()) {
msg += bv;
if (bv.back() != '\n') {
msg += '\n';
}
}
}
// Blank line between body and diff (when no diffstat separator)
if (!msg.empty() && msg.back() == '\n' &&
(msg.size() < 2 || msg[msg.size() - 2] != '\n')) {
msg += '\n';
}
// Diff content
if (!diff.empty()) {
msg += diff;
if (diff.back() != '\n') {
msg += '\n';
}
}
// Trailer (like git's "-- \n2.53.0\n")
msg += "-- \nquilt\n\n";
mbox += msg;
}
if (!write_file(mbox_file, mbox)) {
err_line("Failed to write mbox file: " + mbox_file);
return 1;
}
out("Wrote ");
out(std::to_string(total));
out(" patch");
if (total != 1) out("es");
out(" to ");
out_line(mbox_file);
return 0;
}
// === src/cmd_stubs.cpp ===
// This is free and unencumbered software released into the public domain.
static int not_implemented(const char *name)
{
err("quilt ");
err(name);
err_line(": not implemented");
return 1;
}
int cmd_grep(QuiltState &, int, char **) { return not_implemented("grep"); }
int cmd_setup(QuiltState &, int, char **) { return not_implemented("setup"); }
int cmd_shell(QuiltState &, int, char **) { return not_implemented("shell"); }
// === src/platform_win32.cpp ===
// This is free and unencumbered software released into the public domain.
//
// Win32 platform implementation for quilt.
// Provides main entry point, UTF-16 <-> UTF-8 conversion at system
// boundaries, and Win32 implementations of the platform interface.
//
// This file is compiled only on Windows. POSIX builds use
// platform_posix.cpp instead. No #ifdef guards -- the build system
// selects exactly one platform source.
#ifndef _WIN32
#error "platform_win32.cpp must only be compiled on Windows"
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <shellapi.h>
#include <cctype>
#include <cstdlib>
#include <cstring>
static std::wstring utf8_to_wide(std::string_view s)
{
if (s.empty()) return {};
int len = MultiByteToWideChar(CP_UTF8, 0, s.data(),
checked_cast<int>(std::ssize(s)), nullptr, 0);
if (len <= 0) return {};
std::wstring out(len, L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.data(), checked_cast<int>(std::ssize(s)),
out.data(), len);
return out;
}
static std::string wide_to_utf8(const wchar_t *w, int wlen = -1)
{
if (!w) return {};
if (wlen < 0) wlen = checked_cast<int>(static_cast<ptrdiff_t>(wcslen(w)));
if (wlen == 0) return {};
int len = WideCharToMultiByte(CP_UTF8, 0, w, wlen,
nullptr, 0, nullptr, nullptr);
if (len <= 0) return {};
std::string out(len, '\0');
WideCharToMultiByte(CP_UTF8, 0, w, wlen,
out.data(), len, nullptr, nullptr);
return out;
}
static std::string read_handle(HANDLE h)
{
std::string result;
char buf[4096];
for (;;) {
DWORD n = 0;
if (!ReadFile(h, buf, sizeof(buf), &n, nullptr) || n == 0)
break;
result.append(buf, n);
}
return result;
}
static bool write_handle(HANDLE h, const void *data, size_t len)
{
const char *p = static_cast<const char *>(data);
while (len > 0) {
DWORD written = 0;
if (!WriteFile(h, p, (DWORD)len, &written, nullptr))
return false;
p += written;
len -= written;
}
return true;
}
// Write a UTF-8 string to a handle. When the handle is a console,
// convert to UTF-16 and use WriteConsoleW so that non-ASCII text
// displays correctly. For pipes/files, write raw UTF-8 bytes.
static bool write_console_or_file(HANDLE h, std::string_view s)
{
DWORD mode;
if (GetConsoleMode(h, &mode)) {
std::wstring wide = utf8_to_wide(s);
const wchar_t *p = wide.data();
DWORD remaining = checked_cast<DWORD>(std::ssize(wide));
while (remaining > 0) {
DWORD written = 0;
if (!WriteConsoleW(h, p, remaining, &written, nullptr))
return false;
p += written;
remaining -= written;
}
return true;
}
return write_handle(h, s.data(), s.size());
}
// Create an inheritable pipe. read_end and write_end are set.
// inherit_which: 0 = read end inheritable, 1 = write end inheritable
static bool create_pipe(HANDLE &read_end, HANDLE &write_end,
int inherit_which)
{
SECURITY_ATTRIBUTES sa{};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
if (!CreatePipe(&read_end, &write_end, &sa, 0))
return false;
// Make the non-inherited end non-inheritable
HANDLE &non_inherit = (inherit_which == 0) ? write_end : read_end;
SetHandleInformation(non_inherit, HANDLE_FLAG_INHERIT, 0);
return true;
}
static std::wstring build_cmdline(const std::vector<std::string> &argv)
{
// Windows command-line quoting: wrap each arg in quotes, escape
// internal quotes and backslashes before quotes.
std::wstring cmdline;
for (ptrdiff_t i = 0; i < std::ssize(argv); ++i) {
if (i > 0) cmdline += L' ';
std::wstring arg = utf8_to_wide(argv[checked_cast<size_t>(i)]);
// Check if quoting is needed
bool needs_quote = arg.empty();
for (wchar_t c : arg) {
if (c == L' ' || c == L'\t' || c == L'"') {
needs_quote = true;
break;
}
}
if (!needs_quote) {
cmdline += arg;
continue;
}
cmdline += L'"';
int num_backslashes = 0;
for (wchar_t c : arg) {
if (c == L'\\') {
++num_backslashes;
} else if (c == L'"') {
// Escape preceding backslashes and the quote
for (int j = 0; j < num_backslashes; ++j)
cmdline += L'\\';
cmdline += L'\\';
cmdline += L'"';
num_backslashes = 0;
} else {
num_backslashes = 0;
cmdline += c;
}
}
// Escape trailing backslashes before closing quote
for (int j = 0; j < num_backslashes; ++j)
cmdline += L'\\';
cmdline += L'"';
}
return cmdline;
}
static ProcessResult run_cmd_impl(const std::vector<std::string> &argv,
const char *stdin_data, size_t stdin_len)
{
ProcessResult result{};
if (argv.empty()) {
result.exit_code = -1;
return result;
}
// Create pipes for stdout, stderr, and optionally stdin
HANDLE stdout_rd = INVALID_HANDLE_VALUE, stdout_wr = INVALID_HANDLE_VALUE;
HANDLE stderr_rd = INVALID_HANDLE_VALUE, stderr_wr = INVALID_HANDLE_VALUE;
HANDLE stdin_rd = INVALID_HANDLE_VALUE, stdin_wr = INVALID_HANDLE_VALUE;
if (!create_pipe(stdout_rd, stdout_wr, 1)) { // write end inheritable
result.exit_code = -1;
return result;
}
if (!create_pipe(stderr_rd, stderr_wr, 1)) {
CloseHandle(stdout_rd); CloseHandle(stdout_wr);
result.exit_code = -1;
return result;
}
bool need_stdin = (stdin_data != nullptr);
if (need_stdin) {
if (!create_pipe(stdin_rd, stdin_wr, 0)) { // read end inheritable
CloseHandle(stdout_rd); CloseHandle(stdout_wr);
CloseHandle(stderr_rd); CloseHandle(stderr_wr);
result.exit_code = -1;
return result;
}
}
STARTUPINFOW si{};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = stdout_wr;
si.hStdError = stderr_wr;
si.hStdInput = need_stdin ? stdin_rd : GetStdHandle(STD_INPUT_HANDLE);
PROCESS_INFORMATION pi{};
std::wstring cmdline = build_cmdline(argv);
BOOL ok = CreateProcessW(
nullptr, // lpApplicationName
cmdline.data(), // lpCommandLine (mutable)
nullptr, nullptr, // process/thread security
TRUE, // inherit handles
CREATE_NO_WINDOW, // creation flags
nullptr, // environment
nullptr, // current directory
&si, &pi
);
// Close child-side handles in parent
CloseHandle(stdout_wr);
CloseHandle(stderr_wr);
if (need_stdin) CloseHandle(stdin_rd);
if (!ok) {
result.exit_code = -1;
result.err = "CreateProcessW failed: " + std::to_string(GetLastError());
CloseHandle(stdout_rd);
CloseHandle(stderr_rd);
if (need_stdin) CloseHandle(stdin_wr);
return result;
}
// Write stdin data
if (need_stdin) {
write_handle(stdin_wr, stdin_data, stdin_len);
CloseHandle(stdin_wr);
}
// Read stdout and stderr
result.out = read_handle(stdout_rd);
result.err = read_handle(stderr_rd);
CloseHandle(stdout_rd);
CloseHandle(stderr_rd);
// Wait for process
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD exit_code = 0;
GetExitCodeProcess(pi.hProcess, &exit_code);
result.exit_code = static_cast<int>(exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return result;
}
ProcessResult run_cmd(const std::vector<std::string> &argv)
{
return run_cmd_impl(argv, nullptr, 0);
}
ProcessResult run_cmd_input(const std::vector<std::string> &argv,
std::string_view stdin_data)
{
return run_cmd_impl(argv, stdin_data.data(), stdin_data.size());
}
int run_cmd_tty(const std::vector<std::string> &argv)
{
if (argv.empty()) return -1;
STARTUPINFOW si{};
si.cb = sizeof(si);
// No STARTF_USESTDHANDLES — child inherits console
PROCESS_INFORMATION pi{};
std::wstring cmdline = build_cmdline(argv);
BOOL ok = CreateProcessW(
nullptr, cmdline.data(),
nullptr, nullptr,
FALSE, 0,
nullptr, nullptr,
&si, &pi
);
if (!ok) return -1;
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD exit_code = 0;
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return static_cast<int>(exit_code);
}
std::string read_file(std::string_view path)
{
std::wstring wpath = utf8_to_wide(path);
HANDLE h = CreateFileW(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
nullptr);
if (h == INVALID_HANDLE_VALUE) return {};
std::string result = read_handle(h);
CloseHandle(h);
return result;
}
bool write_file(std::string_view path, std::string_view content)
{
std::wstring wpath = utf8_to_wide(path);
HANDLE h = CreateFileW(wpath.c_str(), GENERIC_WRITE, 0,
nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,
nullptr);
if (h == INVALID_HANDLE_VALUE) return false;
bool ok = write_handle(h, content.data(), content.size());
CloseHandle(h);
return ok;
}
bool append_file(std::string_view path, std::string_view content)
{
std::wstring wpath = utf8_to_wide(path);
HANDLE h = CreateFileW(wpath.c_str(), FILE_APPEND_DATA, 0,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL,
nullptr);
if (h == INVALID_HANDLE_VALUE) return false;
bool ok = write_handle(h, content.data(), content.size());
CloseHandle(h);
return ok;
}
bool copy_file(std::string_view src, std::string_view dst)
{
std::wstring wsrc = utf8_to_wide(src);
std::wstring wdst = utf8_to_wide(dst);
return CopyFileW(wsrc.c_str(), wdst.c_str(), FALSE) != 0;
}
bool rename_path(std::string_view old_path, std::string_view new_path)
{
std::wstring wold = utf8_to_wide(old_path);
std::wstring wnew = utf8_to_wide(new_path);
return MoveFileExW(wold.c_str(), wnew.c_str(),
MOVEFILE_REPLACE_EXISTING) != 0;
}
bool delete_file(std::string_view path)
{
std::wstring wpath = utf8_to_wide(path);
return DeleteFileW(wpath.c_str()) != 0;
}
bool delete_dir_recursive(std::string_view path)
{
std::wstring wpath = utf8_to_wide(path);
std::wstring pattern = wpath + L"\\*";
WIN32_FIND_DATAW fd;
HANDLE hFind = FindFirstFileW(pattern.c_str(), &fd);
if (hFind == INVALID_HANDLE_VALUE) return false;
bool ok = true;
do {
if (wcscmp(fd.cFileName, L".") == 0 ||
wcscmp(fd.cFileName, L"..") == 0)
continue;
std::wstring child = wpath + L"\\" + fd.cFileName;
std::string child_utf8 = wide_to_utf8(child.c_str(),
checked_cast<int>(std::ssize(child)));
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if (!delete_dir_recursive(child_utf8)) ok = false;
} else {
if (!DeleteFileW(child.c_str())) ok = false;
}
} while (FindNextFileW(hFind, &fd));
FindClose(hFind);
if (!RemoveDirectoryW(wpath.c_str())) ok = false;
return ok;
}
bool make_dir(std::string_view path)
{
std::wstring wpath = utf8_to_wide(path);
if (CreateDirectoryW(wpath.c_str(), nullptr)) return true;
return GetLastError() == ERROR_ALREADY_EXISTS;
}
bool make_dirs(std::string_view path)
{
if (path.empty()) return false;
std::string p(path);
// Normalize separators
for (char &c : p) {
if (c == '/') c = '\\';
}
ptrdiff_t start = 1;
if (std::ssize(p) >= 3 && std::isalpha(static_cast<unsigned char>(p[0])) &&
p[1] == ':' && p[2] == '\\') {
start = 3;
} else if (std::ssize(p) >= 2 && p[0] == '\\' && p[1] == '\\') {
// Leave UNC handling to the normal loop after the \\server\share prefix.
ptrdiff_t slash_count = 0;
start = 2;
for (ptrdiff_t i = 2; i < std::ssize(p); ++i) {
if (p[checked_cast<size_t>(i)] == '\\') {
++slash_count;
if (slash_count == 2) {
start = i + 1;
break;
}
}
}
}
// Walk through path components, creating each.
for (ptrdiff_t i = start; i < std::ssize(p); ++i) {
if (p[checked_cast<size_t>(i)] == '\\') {
p[checked_cast<size_t>(i)] = '\0';
std::wstring wp = utf8_to_wide(p.c_str());
if (!CreateDirectoryW(wp.c_str(), nullptr)) {
if (GetLastError() != ERROR_ALREADY_EXISTS)
return false;
}
p[checked_cast<size_t>(i)] = '\\';
}
}
std::wstring wp = utf8_to_wide(p);
if (!CreateDirectoryW(wp.c_str(), nullptr)) {
if (GetLastError() != ERROR_ALREADY_EXISTS)
return false;
}
return true;
}
bool file_exists(std::string_view path)
{
std::wstring wpath = utf8_to_wide(path);
DWORD attr = GetFileAttributesW(wpath.c_str());
return attr != INVALID_FILE_ATTRIBUTES;
}
bool is_directory(std::string_view path)
{
std::wstring wpath = utf8_to_wide(path);
DWORD attr = GetFileAttributesW(wpath.c_str());
if (attr == INVALID_FILE_ATTRIBUTES) return false;
return (attr & FILE_ATTRIBUTE_DIRECTORY) != 0;
}
int64_t file_mtime(std::string_view path)
{
std::wstring wpath = utf8_to_wide(path);
WIN32_FILE_ATTRIBUTE_DATA data;
if (!GetFileAttributesExW(wpath.c_str(), GetFileExInfoStandard, &data))
return -1;
// FILETIME: 100-nanosecond intervals since 1601-01-01
uint64_t ft = (static_cast<uint64_t>(data.ftLastWriteTime.dwHighDateTime) << 32)
| data.ftLastWriteTime.dwLowDateTime;
return static_cast<int64_t>((ft - 116444736000000000ULL) / 10000000ULL);
}
std::vector<DirEntry> list_dir(std::string_view path)
{
std::vector<DirEntry> entries;
std::wstring wpath = utf8_to_wide(path);
std::wstring pattern = wpath + L"\\*";
WIN32_FIND_DATAW fd;
HANDLE hFind = FindFirstFileW(pattern.c_str(), &fd);
if (hFind == INVALID_HANDLE_VALUE) return entries;
do {
if (wcscmp(fd.cFileName, L".") == 0 ||
wcscmp(fd.cFileName, L"..") == 0)
continue;
DirEntry e;
e.name = wide_to_utf8(fd.cFileName);
e.is_dir = (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
entries.push_back(std::move(e));
} while (FindNextFileW(hFind, &fd));
FindClose(hFind);
return entries;
}
static void find_files_impl(const std::wstring &base,
const std::string &prefix,
std::vector<std::string> &out)
{
std::wstring pattern = base + L"\\*";
WIN32_FIND_DATAW fd;
HANDLE hFind = FindFirstFileW(pattern.c_str(), &fd);
if (hFind == INVALID_HANDLE_VALUE) return;
do {
if (wcscmp(fd.cFileName, L".") == 0 ||
wcscmp(fd.cFileName, L"..") == 0)
continue;
std::string name = wide_to_utf8(fd.cFileName);
std::wstring full = base + L"\\" + fd.cFileName;
std::string rel = prefix.empty() ? name : prefix + "/" + name;
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
find_files_impl(full, rel, out);
} else {
out.push_back(rel);
}
} while (FindNextFileW(hFind, &fd));
FindClose(hFind);
}
std::vector<std::string> find_files_recursive(std::string_view dir)
{
std::vector<std::string> result;
std::wstring wdir = utf8_to_wide(dir);
find_files_impl(wdir, "", result);
return result;
}
std::string make_temp_dir()
{
wchar_t tmp_path[MAX_PATH + 1];
if (!GetTempPathW(MAX_PATH + 1, tmp_path)) return {};
DWORD pid = GetCurrentProcessId();
static unsigned counter = 0;
wchar_t tmp_dir[MAX_PATH + 1];
for (int attempt = 0; attempt < 100; ++attempt) {
unsigned n = counter++;
swprintf(tmp_dir, MAX_PATH, L"%sqlt%lu_%u", tmp_path, (unsigned long)pid, n);
if (CreateDirectoryW(tmp_dir, nullptr)) {
std::string result = wide_to_utf8(tmp_dir);
for (char &c : result)
if (c == '\\') c = '/';
return result;
}
}
return {};
}
std::string get_env(std::string_view name)
{
std::wstring wname = utf8_to_wide(name);
// First call to get required buffer size
DWORD len = GetEnvironmentVariableW(wname.c_str(), nullptr, 0);
if (len == 0) return {};
std::wstring buf(len, L'\0');
GetEnvironmentVariableW(wname.c_str(), buf.data(), len);
// Remove trailing null
if (!buf.empty() && buf.back() == L'\0') buf.pop_back();
return wide_to_utf8(buf.c_str(), checked_cast<int>(std::ssize(buf)));
}
void set_env(std::string_view name, std::string_view value)
{
std::wstring wname = utf8_to_wide(name);
std::wstring wvalue = utf8_to_wide(value);
SetEnvironmentVariableW(wname.c_str(), wvalue.c_str());
}
std::string get_home_dir()
{
std::string home = get_env("HOME");
if (!home.empty()) return home;
std::string up = get_env("USERPROFILE");
if (!up.empty()) return up;
std::string hd = get_env("HOMEDRIVE");
std::string hp = get_env("HOMEPATH");
if (!hd.empty() && !hp.empty()) return hd + hp;
return {};
}
std::string get_system_quiltrc()
{
wchar_t buf[MAX_PATH];
DWORD len = GetModuleFileNameW(NULL, buf, MAX_PATH);
if (len == 0 || len >= MAX_PATH) return {};
std::wstring path(buf, len);
// Strip exe filename
auto pos = path.rfind(L'\\');
if (pos == std::wstring::npos) return {};
path.resize(pos);
// Strip parent directory (e.g. bin/)
pos = path.rfind(L'\\');
if (pos == std::wstring::npos) return {};
path.resize(pos);
path += L"\\etc\\quilt.quiltrc";
std::string result = wide_to_utf8(path.c_str(),
checked_cast<int>(std::ssize(path)));
for (char &c : result) {
if (c == '\\') c = '/';
}
return result;
}
std::string get_cwd()
{
DWORD len = GetCurrentDirectoryW(0, nullptr);
if (len == 0) return {};
std::wstring buf(len, L'\0');
GetCurrentDirectoryW(len, buf.data());
// Remove trailing null
if (!buf.empty() && buf.back() == L'\0') buf.pop_back();
std::string result = wide_to_utf8(buf.c_str(), checked_cast<int>(std::ssize(buf)));
// Normalize backslashes to forward slashes for consistency
for (char &c : result) {
if (c == '\\') c = '/';
}
return result;
}
bool set_cwd(std::string_view path)
{
std::wstring wpath = utf8_to_wide(path);
return SetCurrentDirectoryW(wpath.c_str()) != 0;
}
void fd_write_stdout(std::string_view s)
{
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
if (h != INVALID_HANDLE_VALUE)
write_console_or_file(h, s);
}
void fd_write_stderr(std::string_view s)
{
HANDLE h = GetStdHandle(STD_ERROR_HANDLE);
if (h != INVALID_HANDLE_VALUE)
write_console_or_file(h, s);
}
bool stdout_is_tty()
{
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
return h != INVALID_HANDLE_VALUE && GetFileType(h) == FILE_TYPE_CHAR;
}
std::string read_stdin()
{
HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
if (h == INVALID_HANDLE_VALUE) return {};
return read_handle(h);
}
int64_t current_time()
{
return _time64(nullptr);
}
DateTime local_time(int64_t timestamp)
{
__time64_t t = static_cast<__time64_t>(timestamp);
struct tm local_tm, utc_tm;
_localtime64_s(&local_tm, &t);
_gmtime64_s(&utc_tm, &t);
long local_sec = local_tm.tm_hour * 3600L + local_tm.tm_min * 60L + local_tm.tm_sec;
long utc_sec = utc_tm.tm_hour * 3600L + utc_tm.tm_min * 60L + utc_tm.tm_sec;
long diff = local_sec - utc_sec;
int day_diff = local_tm.tm_yday - utc_tm.tm_yday;
if (day_diff > 1) day_diff = -1;
if (day_diff < -1) day_diff = 1;
diff += day_diff * 86400L;
return {
local_tm.tm_year + 1900,
local_tm.tm_mon + 1,
local_tm.tm_mday,
local_tm.tm_hour,
local_tm.tm_min,
local_tm.tm_sec,
local_tm.tm_wday,
static_cast<int>(diff),
};
}
int main(int, char **)
{
// Use GetCommandLineW + CommandLineToArgvW for reliable parsing
int argc = 0;
wchar_t **wargv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (!wargv) return 1;
// Convert to UTF-8
std::vector<std::string> args_storage;
args_storage.reserve(argc);
for (int i = 0; i < argc; ++i)
args_storage.push_back(wide_to_utf8(wargv[i]));
LocalFree(wargv);
std::vector<char *> argv_ptrs;
argv_ptrs.reserve(argc + 1);
for (auto &a : args_storage)
argv_ptrs.push_back(a.data());
argv_ptrs.push_back(nullptr);
return quilt_main(argc, argv_ptrs.data());
}