// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Checked integer cast: asserts value is representable in the target type. template constexpr To checked_cast(From value) { static_assert(std::is_integral_v && std::is_integral_v); assert(std::in_range(value)); return static_cast(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(pos)); return r == std::string_view::npos ? ptrdiff_t{-1} : static_cast(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(pos)); return r == std::string_view::npos ? ptrdiff_t{-1} : static_cast(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(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 series; // ordered patch names from series file std::vector applied; // applied patch names from .pc/applied-patches std::map patch_strip_level; // per-patch strip level from series std::set patch_reversed; // patches marked -R in series std::map 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 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 split_lines(std::string_view s); std::vector split_on_whitespace(std::string_view s); std::vector 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 *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 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 *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(std::ssize(q.patches_dir))] == '/') { return name.substr(checked_cast(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 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 read_series(std::string_view path, std::map *strip_levels, std::set *reversed); bool write_series(std::string_view path, std::span patches, const std::map &strip_levels, const std::set &reversed); std::vector read_applied(std::string_view path); bool write_applied(std::string_view path, std::span 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 #include #include #include // Process execution struct ProcessResult { int exit_code; std::string out; std::string err; }; ProcessResult run_cmd(const std::vector &argv); ProcessResult run_cmd_input(const std::vector &argv, std::string_view stdin_data); int run_cmd_tty(const std::vector &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 list_dir(std::string_view path); // Find all regular files recursively under a directory (relative paths) std::vector 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(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 QuiltState::find_in_series(std::string_view patch) const { for (ptrdiff_t i = 0; i < std::ssize(series); ++i) { if (series[checked_cast(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(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(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 split_lines(std::string_view s) { std::vector 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(pos); if (end > 0 && s[end - 1] == '\r') --end; lines.emplace_back(s.substr(0, end)); s.remove_prefix(checked_cast(pos + 1)); } return lines; } std::vector split_on_whitespace(std::string_view s) { std::vector tokens; ptrdiff_t i = 0; while (i < std::ssize(s)) { while (i < std::ssize(s) && (s[checked_cast(i)] == ' ' || s[checked_cast(i)] == '\t')) ++i; if (i >= std::ssize(s)) break; ptrdiff_t start = i; while (i < std::ssize(s) && s[checked_cast(i)] != ' ' && s[checked_cast(i)] != '\t') ++i; tokens.emplace_back(s.substr(checked_cast(start), checked_cast(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(i)] == '{') { ++i; // skip '{' while (i < std::ssize(s) && s[checked_cast(i)] != '}') { name += s[checked_cast(i++)]; } if (i < std::ssize(s)) ++i; // skip '}' } else { while (i < std::ssize(s) && is_varname_char(s[checked_cast(i)])) { name += s[checked_cast(i++)]; } } if (name.empty()) return "$"; return get_env(name); } std::vector shell_split(std::string_view s) { std::vector tokens; ptrdiff_t i = 0; while (i < std::ssize(s)) { // Skip whitespace between tokens while (i < std::ssize(s) && (s[checked_cast(i)] == ' ' || s[checked_cast(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(i)] != ' ' && s[checked_cast(i)] != '\t') { if (s[checked_cast(i)] == '\'') { // Single-quoted: literal, no escapes, no variable expansion ++i; while (i < std::ssize(s) && s[checked_cast(i)] != '\'') tok += s[checked_cast(i++)]; if (i < std::ssize(s)) ++i; // skip closing ' } else if (s[checked_cast(i)] == '"') { // Double-quoted: backslash escapes and variable expansion ++i; while (i < std::ssize(s) && s[checked_cast(i)] != '"') { if (s[checked_cast(i)] == '\\' && i + 1 < std::ssize(s)) { char next = s[checked_cast(i + 1)]; if (next == '"' || next == '\\' || next == '$') { tok += next; i += 2; continue; } } if (s[checked_cast(i)] == '$') { ++i; tok += expand_var(s, i); continue; } tok += s[checked_cast(i++)]; } if (i < std::ssize(s)) ++i; // skip closing " } else if (s[checked_cast(i)] == '$') { // Unquoted variable expansion ++i; tok += expand_var(s, i); } else if (s[checked_cast(i)] == '\\' && i + 1 < std::ssize(s)) { // Unquoted backslash escape tok += s[checked_cast(i + 1)]; i += 2; } else { tok += s[checked_cast(i++)]; } } if (!tok.empty()) { tokens.push_back(std::move(tok)); } } return tokens; } std::vector read_series(std::string_view path, std::map *strip_levels, std::set *reversed) { std::vector 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(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(i)] == "-p" && i + 1 < std::ssize(tokens)) { strip = checked_cast(parse_int(tokens[checked_cast(i + 1)])); ++i; } else if (tokens[checked_cast(i)].starts_with("-p") && std::ssize(tokens[checked_cast(i)]) > 2) { strip = checked_cast(parse_int(tokens[checked_cast(i)].substr(2))); } else if (tokens[checked_cast(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 patches, const std::map &strip_levels, const std::set &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 read_applied(std::string_view path) { std::vector 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 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 parse_quiltrc(std::string_view content) { std::map 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(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(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 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 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 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(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 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(std::ssize(clean_argv)); // Handle no arguments if (clean_argc < 2) { err_line("Usage: quilt [--quiltrc file] [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] [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(max_len + 2 - std::ssize(std::string_view(commands[i].name))), ' '); line += commands[i].description; out_line(line); } out_line(""); out_line("Use \"quilt --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(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 final_argv_storage; std::vector 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(i)]); } for (auto &s : final_argv_storage) { final_argv.push_back(const_cast(s.c_str())); } // Dispatch return found->fn(q, checked_cast(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 #include #include #include std::optional 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 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(i)] == '\n') { fl.lines.push_back(content.substr(checked_cast(start), checked_cast(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(start), checked_cast(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 backtrack_trace( const std::vector> &trace, ptrdiff_t final_d, ptrdiff_t x, ptrdiff_t y, ptrdiff_t offset) { std::vector ops; for (ptrdiff_t d = final_d; d > 0; --d) { const auto &prev_v = trace[checked_cast(d)]; ptrdiff_t k = x - y; ptrdiff_t prev_k; if (k == -d || (k != d && prev_v[checked_cast(offset + k - 1)] < prev_v[checked_cast(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(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 myers_diff( std::span old_lines, std::span 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 ops; ops.reserve(checked_cast(m)); for (ptrdiff_t j = 0; j < m; ++j) ops.push_back({'I', -1, j}); return ops; } if (m == 0) { std::vector ops; ops.reserve(checked_cast(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> trace; std::vector v(checked_cast(v_size), -1); v[checked_cast(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(offset + k - 1)] < v[checked_cast(offset + k + 1)])) { x = v[checked_cast(offset + k + 1)]; // move down (insert) } else { x = v[checked_cast(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(x)] == new_lines[checked_cast(y)]) { ++x; ++y; } v[checked_cast(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(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(best_x)); auto tail_new = new_lines.subspan(checked_cast(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 patience_diff( std::span old_lines, std::span 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 ops; for (ptrdiff_t j = 0; j < m; ++j) ops.push_back({'I', -1, j}); return ops; } if (m == 0) { std::vector 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(prefix)] == new_lines[checked_cast(prefix)]) ++prefix; ptrdiff_t suffix = 0; while (suffix < (n - prefix) && suffix < (m - prefix) && old_lines[checked_cast(n - 1 - suffix)] == new_lines[checked_cast(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 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 line_map; for (ptrdiff_t i = 0; i < inner_old_len; ++i) { auto &info = line_map[old_lines[checked_cast(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(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 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 cards; std::vector 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(pile_tops[checked_cast(mid)])].new_idx < match.new_idx) lo = mid + 1; else hi = mid; } ptrdiff_t back = (lo > 0) ? pile_tops[checked_cast(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(lo)] = card_idx; } // Reconstruct LIS by walking back-pointers from the last pile's top. std::vector anchors; if (!pile_tops.empty()) { ptrdiff_t idx = pile_tops.back(); while (idx >= 0) { const auto &c = cards[checked_cast(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(prefix), checked_cast(inner_old_len)); auto inner_new = new_lines.subspan(checked_cast(prefix), checked_cast(inner_new_len)); auto inner_ops = myers_diff(inner_old, inner_new, DiffAlgorithm::minimal); // Assemble: prefix + inner + suffix std::vector 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 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(prefix + prev_old), checked_cast(gap_old_len)); auto gap_new = new_lines.subspan(checked_cast(prefix + prev_new), checked_cast(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(prefix + prev_old), checked_cast(tail_old_len)); auto tail_new = new_lines.subspan(checked_cast(prefix + prev_new), checked_cast(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 histogram_diff_impl( std::span old_lines, std::span 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 ops; for (ptrdiff_t j = 0; j < m; ++j) ops.push_back({'I', -1, j}); return ops; } if (m == 0) { std::vector 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(prefix)] == new_lines[checked_cast(prefix)]) ++prefix; ptrdiff_t suffix = 0; while (suffix < (n - prefix) && suffix < (m - prefix) && old_lines[checked_cast(n - 1 - suffix)] == new_lines[checked_cast(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 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(prefix), checked_cast(inner_old_len)); auto inner_new = new_lines.subspan(checked_cast(prefix), checked_cast(inner_new_len)); auto inner_ops = myers_diff(inner_old, inner_new, DiffAlgorithm::minimal); std::vector 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 positions; // interior-relative positions in old }; std::unordered_map hist; std::vector old_count(checked_cast(inner_old_len)); for (ptrdiff_t i = 0; i < inner_old_len; ++i) { auto &entry = hist[old_lines[checked_cast(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(prefix + i)]); old_count[checked_cast(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(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(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(prefix + oi - back - 1)] == new_lines[checked_cast(prefix + j - back - 1)]) { ++back; int occ = old_count[checked_cast(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(prefix + oi + fwd + 1)] == new_lines[checked_cast(prefix + j + fwd + 1)]) { ++fwd; int occ = old_count[checked_cast(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(prefix), checked_cast(inner_old_len)); auto inner_new = new_lines.subspan(checked_cast(prefix), checked_cast(inner_new_len)); if (best_len == 0) { auto inner_ops = myers_diff(inner_old, inner_new, DiffAlgorithm::minimal); std::vector 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 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(prefix), checked_cast(best_old_start)); auto before_new = new_lines.subspan(checked_cast(prefix), checked_cast(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(prefix + after_old_start), checked_cast(after_old_len)); auto after_new = new_lines.subspan(checked_cast(prefix + after_new_start), checked_cast(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 histogram_diff( std::span old_lines, std::span 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 ops; // the operations in this hunk (including context) }; static std::vector build_hunks(const std::vector &ops, ptrdiff_t context_lines) { std::vector 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 changes; for (ptrdiff_t i = 0; i < std::ssize(ops); ++i) { if (ops[checked_cast(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 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(i)].first - merged.back().last <= 2 * context_lines) { merged.back().last = changes[checked_cast(i)].last; } else { merged.push_back(changes[checked_cast(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(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(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 old_lines, std::span new_lines, bool old_has_trailing_nl, bool new_has_trailing_nl, const std::vector &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(op.old_idx)]; result += '\n'; if (!old_has_trailing_nl) { result += "\\ No newline at end of file\n"; } result += '+'; result += new_lines[checked_cast(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(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(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(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 old_lines, std::span new_lines, bool old_has_trailing_nl, bool new_has_trailing_nl, const std::vector &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 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(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(op.old_idx)], onl}); new_side.push_back({' ', new_lines[checked_cast(op.new_idx)], onl}); ++k; } else { // Collect consecutive D then I runs ptrdiff_t ds = k; while (k < num_ops && hunk.ops[checked_cast(k)].type == 'D') ++k; ptrdiff_t de = k; // exclusive while (k < num_ops && hunk.ops[checked_cast(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(j)]; bool onl = (dop.old_idx == old_total - 1 && !old_has_trailing_nl); old_side.push_back({is_change ? '!' : '-', old_lines[checked_cast(dop.old_idx)], onl}); } for (ptrdiff_t j = de; j < ie; ++j) { auto &iop = hunk.ops[checked_cast(j)]; bool onl = (iop.new_idx == new_total - 1 && !new_has_trailing_nl); new_side.push_back({is_change ? '!' : '+', new_lines[checked_cast(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 *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 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(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(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 #include #include // ── 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 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 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(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(tab)); } // Trim trailing whitespace while (!rest.empty() && (rest.back() == ' ' || rest.back() == '\r')) { rest = rest.substr(0, checked_cast(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 parse_patch(std::string_view text, int strip_level, bool reverse) { std::vector 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(i)].starts_with("--- ")) { ++i; continue; } // Peek ahead for "+++ " if (i + 1 >= n || !lines[checked_cast(i + 1)].starts_with("+++ ")) { ++i; continue; } PatchFile pf; std::string raw_old = extract_path(std::string_view(lines[checked_cast(i)]).substr(4)); std::string raw_new = extract_path(std::string_view(lines[checked_cast(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(i)].starts_with("@@ ")) { PatchHunk hunk; // Parse @@ -old_start[,old_count] +new_start[,new_count] @@ std::string_view hdr = std::string_view(lines[checked_cast(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(pos), checked_cast(comma - pos))); hunk.old_count = parse_int(hdr.substr(checked_cast(comma + 1), checked_cast(plus_pos - comma - 2))); } else if (space >= 0 && space < plus_pos) { hunk.old_start = parse_int(hdr.substr(checked_cast(pos), checked_cast(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(pos), checked_cast(comma - pos))); hunk.new_count = parse_int(hdr.substr(checked_cast(comma + 1), checked_cast(end_at - comma - 1))); } else { hunk.new_start = parse_int(hdr.substr(checked_cast(pos), checked_cast(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(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 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(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(i)] == '\n') { ptrdiff_t end = i; if (end > start && content[checked_cast(end - 1)] == '\r') --end; fc.lines.emplace_back(content.substr(checked_cast(start), checked_cast(end - start))); start = i + 1; } } if (start < len) { std::string tail(content.substr(checked_cast(start), checked_cast(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 get_old_pattern(const PatchHunk &hunk) { std::vector 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 file_lines, ptrdiff_t pos, const std::vector &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(fuzz), prefix_ctx); ptrdiff_t suffix_fuzz = std::min(static_cast(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(file_pos)] != pattern[checked_cast(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 file_lines, const PatchHunk &hunk, const std::vector &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(fuzz), ctx.prefix); ptrdiff_t suffix_fuzz = std::min(static_cast(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 get_new_lines(const PatchHunk &hunk) { std::vector 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 file_lines, bool has_trailing_newline, const PatchFile &pf, const std::vector &hunk_positions, const std::vector &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(h)]; if (pos < 0) continue; // rejected hunk, skip const auto &hunk = pf.hunks[checked_cast(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(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(j)]; output += '\n'; } // Write replacement lines (trimmed by fuzz) for (ptrdiff_t j = new_start; j < new_end; ++j) { output += new_lines[checked_cast(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(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 file_lines, bool has_trailing_newline, const PatchFile &pf, const std::vector &hunk_positions, const std::vector &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(h)]; ptrdiff_t pos = hunk_positions[checked_cast(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(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(j)]; output += '\n'; } for (ptrdiff_t j = new_start; j < new_end; ++j) { output += new_lines[checked_cast(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(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(hi)][0]; if (prefix == ' ') { // Context line — emit the file's actual line if (file_pos < file_len) { output += file_lines[checked_cast(file_pos)]; output += '\n'; ++file_pos; } ++hi; } else { // Changed region — collect contiguous -/+ lines std::vector old_lines, new_change; while (hi < hunk_len && hunk.lines[checked_cast(hi)][0] == '-') { old_lines.push_back(std::string_view(hunk.lines[checked_cast(hi)]).substr(1)); ++hi; } while (hi < hunk_len && hunk.lines[checked_cast(hi)][0] == '+') { new_change.push_back(std::string_view(hunk.lines[checked_cast(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(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(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 &rejected) { std::string result; bool has_any = false; for (ptrdiff_t h = 0; h < std::ssize(pf.hunks); ++h) { if (!rejected[checked_cast(h)]) continue; const auto &hunk = pf.hunks[checked_cast(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 all_rejected(checked_cast(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 hunk_positions(checked_cast(std::ssize(pf.hunks)), -1); std::vector hunk_fuzz(checked_cast(std::ssize(pf.hunks))); std::vector rejected(checked_cast(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(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(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(h)] = { std::min(static_cast(fuzz_used), ctx.prefix), std::min(static_cast(fuzz_used), ctx.suffix) }; auto &fz = hunk_fuzz[checked_cast(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(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(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 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 parse_patch_files(std::string_view content, int strip = 1) { std::vector 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(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(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(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(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(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(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(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(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(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(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 r_storage; r_storage.push_back("refresh"); for (auto &e : extra) r_storage.push_back(e); std::vector r_argv; for (auto &s : r_storage) r_argv.push_back(s.data()); int rr = cmd_refresh(q, checked_cast(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 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 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 reannotate_lines(std::span old_lines, std::span old_annotations, std::span new_lines, std::string_view annotation) { const ptrdiff_t m = std::ssize(old_lines); const ptrdiff_t n = std::ssize(new_lines); std::vector> dp(checked_cast(m + 1), std::vector(checked_cast(n + 1), 0)); for (ptrdiff_t i = m; i-- > 0;) { for (ptrdiff_t j = n; j-- > 0;) { if (old_lines[checked_cast(i)] == new_lines[checked_cast(j)]) { dp[checked_cast(i)][checked_cast(j)] = dp[checked_cast(i + 1)][checked_cast(j + 1)] + 1; } else { dp[checked_cast(i)][checked_cast(j)] = std::max(dp[checked_cast(i + 1)][checked_cast(j)], dp[checked_cast(i)][checked_cast(j + 1)]); } } } std::vector result; result.reserve(checked_cast(n)); ptrdiff_t i = 0; ptrdiff_t j = 0; while (i < m || j < n) { if (i < m && j < n && old_lines[checked_cast(i)] == new_lines[checked_cast(j)]) { result.push_back(i < std::ssize(old_annotations) ? old_annotations[checked_cast(i)] : ""); ++i; ++j; } else if (j < n && (i == m || dp[checked_cast(i)][checked_cast(j + 1)] >= dp[checked_cast(i + 1)][checked_cast(j)])) { result.emplace_back(annotation); ++j; } else { ++i; } } return result; } std::optional 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 patches; std::vector 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 annotations(checked_cast(std::ssize(read_lines(files.front()))), ""); for (ptrdiff_t i = 0; i < std::ssize(patches); ++i) { annotations = reannotate_lines(read_lines(files[checked_cast(i)]), annotations, read_lines(files[checked_cast(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(i)] : ""; out(annotations[checked_cast(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(i)]) + "\n"); } return 0; } // === src/cmd_patch.cpp === // This is free and unencumbered software released into the public domain. #include #include #include 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(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// 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 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 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 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 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(i)].starts_with("--- ")) ++i; if (i < n) { result += "*** " + lines[checked_cast(i)].substr(4) + "\n"; ++i; } if (i < n && lines[checked_cast(i)].starts_with("+++ ")) { result += "--- " + lines[checked_cast(i)].substr(4) + "\n"; ++i; } while (i < n) { if (!lines[checked_cast(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(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(parse_int(hdr.substr(checked_cast(p), checked_cast(c - p)))); oc = checked_cast(parse_int(hdr.substr(checked_cast(c + 1), checked_cast(pp - c - 2)))); } else { os = checked_cast(parse_int(hdr.substr(checked_cast(p), checked_cast(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(parse_int(hdr.substr(checked_cast(p), checked_cast(c - p)))); nc = checked_cast(parse_int(hdr.substr(checked_cast(c + 1), checked_cast(end - c - 1)))); } else { ns = checked_cast(parse_int(hdr.substr(checked_cast(p), checked_cast(end - p)))); } } } } ++i; // Collect unified hunk body lines with their types struct UL { char type; std::string text; }; std::vector
    body; while (i < n && !lines[checked_cast(i)].starts_with("@@ ")) { std::string_view ln = lines[checked_cast(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> old_side, new_side; for (ptrdiff_t k = 0; k < std::ssize(body); ) { if (body[checked_cast(k)].type == ' ') { old_side.push_back({' ', body[checked_cast(k)].text}); new_side.push_back({' ', body[checked_cast(k)].text}); ++k; } else if (body[checked_cast(k)].type == '-') { ptrdiff_t ds = k; while (k < std::ssize(body) && body[checked_cast(k)].type == '-') ++k; ptrdiff_t as = k; while (k < std::ssize(body) && body[checked_cast(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(m)].text}); for (ptrdiff_t m = as; m < k; ++m) new_side.push_back({change ? '!' : '+', body[checked_cast(m)].text}); } else if (body[checked_cast(k)].type == '+') { new_side.push_back({'+', body[checked_cast(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 opts) { for (ptrdiff_t i = 0; i < std::ssize(opts); ++i) { const auto &o = opts[checked_cast(i)]; if (o.starts_with("-U") && std::ssize(o) > 2) { return checked_cast(parse_int(std::string_view(o).substr(2))); } if (o == "-U" && i + 1 < std::ssize(opts)) { return checked_cast(parse_int(opts[checked_cast(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 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 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 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 split_patch_by_file(std::string_view content) { std::map 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(tab)); // Strip leading directory component (e.g., "dir.orig/") auto slash = str_find(rest, '/'); if (slash >= 0) { current_file = trim(rest.substr(checked_cast(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(tab)); auto slash = str_find(rest, '/'); if (slash >= 0) { current_file = trim(rest.substr(checked_cast(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 &dst, std::set &seen, std::span src) { for (const auto &file : src) { if (seen.insert(file).second) { dst.push_back(file); } } } static std::vector collect_files_for_patches( const QuiltState &q, std::span patches) { std::vector files; std::set seen; for (const auto &patch : patches) { append_unique_files(files, seen, files_in_patch(q, patch)); } return files; } static std::vector patch_range_for_diff(const QuiltState &q, std::string_view last_patch) { if (last_patch.empty()) { return q.applied; } std::vector 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 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 &tracked, std::span file_filter) { if (file_filter.empty()) { return; } std::vector 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 stats; auto lines = split_lines(diff); for (ptrdiff_t i = 0; i < std::ssize(lines); ++i) { const auto &line = lines[checked_cast(i)]; // Detect file header: "--- a/file" followed by "+++ b/file" if (line.starts_with("--- ") && i + 1 < std::ssize(lines) && lines[checked_cast(i + 1)].starts_with("+++ ")) { const auto &plus_line = lines[checked_cast(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(tab)); // Strip one leading path component (a/ or b/ prefix) auto slash = str_find(name, '/'); if (slash >= 0) name = name.substr(checked_cast(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(i)].substr(4); tab = str_find(name, '\t'); if (tab >= 0) name = name.substr(0, checked_cast(tab)); slash = str_find(name, '/'); if (slash >= 0) name = name.substr(checked_cast(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(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(1), static_cast(72) - used); // Scale factor for bar graph double scale = (max_changes > bar_width) ? static_cast(bar_width) / static_cast(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( static_cast(s.added) * scale + 0.5); ptrdiff_t minus_bars = static_cast( static_cast(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( static_cast(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(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(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(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(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(parse_int(context_num)); } else if (diff_type == "U") { ctx_lines = checked_cast(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(dot)) + "-2" + old_name.substr(checked_cast(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 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 shadowed; std::map 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 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(plus)), ','); std::string_view num_str; if (comma >= 0) { num_str = std::string_view(dl).substr( checked_cast(plus) + 1, checked_cast(comma) - 1); } else { auto sp = str_find( std::string_view(dl).substr( checked_cast(plus)), ' '); if (sp >= 0) { num_str = std::string_view(dl).substr( checked_cast(plus) + 1, checked_cast(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(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(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(std::ssize(clean_header) - 1)] == '\n' && clean_header[checked_cast(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 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(parse_int(context_num)); } else if (diff_type == "U") { ctx_lines = checked_cast(parse_int(context_num)); } // Build diff command base for external diff utility (empty = use builtin) bool convert_to_context = false; std::vector 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 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 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 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 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 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 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 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 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 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(i)].starts_with("Index:") || lines[checked_cast(i)].starts_with("--- ") || lines[checked_cast(i)].starts_with("diff ") || lines[checked_cast(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(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(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(*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 patchfiles; for (int i = 1; i < argc; ++i) { std::string_view arg = argv[i]; if (arg == "-p" && i + 1 < argc) { strip_level = checked_cast(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/, 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(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(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(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(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: \n" " \n" "Author: \n" "Origin: , \n" "Bug: \n" "Bug-Debian: https://bugs.debian.org/\n" "Forwarded: \n" "Applied-Upstream: \n" "Last-Update: \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 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 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 all_files; for (const auto &patch : patches_to_show) { std::vector 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 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// 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(parse_int(argv[++i])); } else if (arg.starts_with("-p") && arg.size() > 2 && arg[2] >= '0' && arg[2] <= '9') { strip_level = checked_cast(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(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(dot)); ext = old_name.substr(checked_cast(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(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(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(*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 #include #include #include #include #include #include namespace { struct LineRanges { bool computed = false; std::vector left; std::vector right; }; struct GraphNode { int number = 0; std::string name; std::map files; std::vector attrs; }; struct EdgeData { std::vector names; }; using EdgeKey = std::pair; 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(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(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(parse_int(match[1].str())); int old_count = parse_hunk_count(match[2]); int new_start = checked_cast(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 next_node_for_file(const std::vector &nodes, int index, std::string_view file) { for (int i = index + 1; i < std::ssize(nodes); ++i) { if (nodes[checked_cast(i)].files.contains(std::string(file))) { return i; } } return std::nullopt; } static void compute_ranges(const QuiltState &q, std::vector &nodes, int index, std::string_view file, int context_lines) { auto it = nodes[checked_cast(index)].files.find(std::string(file)); if (it == nodes[checked_cast(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(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(*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 &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(from)].files[file_key].right; const auto &b = nodes[checked_cast(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(ia)] < b[checked_cast(ib)]) { if ((rem_b % 2) == 1) return true; ++ia; } else if (a[checked_cast(ia)] > b[checked_cast(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 &edges, int earlier, int later, std::string_view file) { auto &edge = edges[{earlier, later}]; edge.names.emplace_back(file); } static std::set collect_reachable(const std::map &edges, int start, bool forward) { std::map> 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 seen; std::vector 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> &adjacency, EdgeKey skip_edge) { std::vector stack = {from}; std::set 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 &edges) { std::map> adjacency; for (const auto &[key, value] : edges) { (void)value; adjacency[key.first].push_back(key.second); } std::vector 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(std::abs(to - from) + 3)); return "len=\"" + value.str() + "\""; } static std::string render_dot(const std::vector &nodes, std::map edges, std::set 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 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(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 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(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(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 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(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(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 nodes; nodes.reserve(checked_cast(std::ssize(q.applied))); for (ptrdiff_t i = 0; i < std::ssize(q.applied); ++i) { const std::string &patch = q.applied[checked_cast(i)]; auto files = files_in_patch(q, patch); std::ranges::sort(files); GraphNode node; node.number = checked_cast(i); node.name = patch; for (const auto &file : files) { node.files.emplace(file, LineRanges{}); } nodes.push_back(std::move(node)); } std::set 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 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> files_seen; std::map 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 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 reachable = collect_reachable(edges, selected_index, true); std::set 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 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(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(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(static_cast(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(at + 1), checked_cast(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 to_addrs; std::vector cc_addrs; std::vector bcc_addrs; std::vector 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(total)); std::string mbox; for (ptrdiff_t i = first_idx; i <= last_idx; ++i) { const std::string &patch = q.series[checked_cast(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(subj_line)]).empty()) { subj_line++; } if (subj_line < std::ssize(hdr_lines)) { subject_text = trim(hdr_lines[checked_cast(subj_line)]); // Remaining lines become body for (ptrdiff_t j = subj_line + 1; j < std::ssize(hdr_lines); ++j) { body += hdr_lines[checked_cast(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(i - first_idx + 1); subject_prefix = std::format("[{} {:0{}d}/{}]", prefix, seq, width, checked_cast(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(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 #include #include #include #include static std::wstring utf8_to_wide(std::string_view s) { if (s.empty()) return {}; int len = MultiByteToWideChar(CP_UTF8, 0, s.data(), checked_cast(std::ssize(s)), nullptr, 0); if (len <= 0) return {}; std::wstring out(len, L'\0'); MultiByteToWideChar(CP_UTF8, 0, s.data(), checked_cast(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(static_cast(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(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(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 &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(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 &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(exit_code); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return result; } ProcessResult run_cmd(const std::vector &argv) { return run_cmd_impl(argv, nullptr, 0); } ProcessResult run_cmd_input(const std::vector &argv, std::string_view stdin_data) { return run_cmd_impl(argv, stdin_data.data(), stdin_data.size()); } int run_cmd_tty(const std::vector &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(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(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(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(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(i)] == '\\') { p[checked_cast(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(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(data.ftLastWriteTime.dwHighDateTime) << 32) | data.ftLastWriteTime.dwLowDateTime; return static_cast((ft - 116444736000000000ULL) / 10000000ULL); } std::vector list_dir(std::string_view path) { std::vector 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 &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 find_files_recursive(std::string_view dir) { std::vector 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(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(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(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(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 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 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()); }