patchkit-0.1.8/.cargo_vcs_info.json0000644000000001360000000000100126510ustar { "git": { "sha1": "fdbad8c3bf3c9b9021c579c935c60d7123cd8797" }, "path_in_vcs": "" }patchkit-0.1.8/.github/CODEOWNERS000064400000000000000000000001661046102023000143770ustar 00000000000000* @jelmer # Release robot dulwich/contrib/release_robot.py @mikofski dulwich/contrib/test_release_robot.py @mikofski patchkit-0.1.8/.github/dependabot.yml000064400000000000000000000010351046102023000156300ustar 00000000000000# Keep GitHub Actions up to date with GitHub's Dependabot... # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly patchkit-0.1.8/.github/workflows/rust.yml000064400000000000000000000004121046102023000165530ustar 00000000000000name: Rust on: push: pull_request: env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose patchkit-0.1.8/.gitignore000064400000000000000000000000211046102023000134220ustar 00000000000000/target *.swp *~ patchkit-0.1.8/Cargo.toml0000644000000021620000000000100106500ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "patchkit" version = "0.1.8" authors = ["Jelmer Vernooij "] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "A library for parsing and manipulating patch files" homepage = "https://github.com/breezy-team/patchkit" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/breezy-team/patchkit" [lib] name = "patchkit" path = "src/lib.rs" [dependencies.chrono] version = "0.4" [dependencies.lazy-regex] version = ">=2" [dependencies.lazy_static] version = "1" [dependencies.once_cell] version = "1.19.0" [dependencies.regex] version = "1" patchkit-0.1.8/Cargo.toml.orig000064400000000000000000000006331046102023000143320ustar 00000000000000[package] name = "patchkit" version = "0.1.8" edition = "2021" license = "Apache-2.0" description = "A library for parsing and manipulating patch files" repository = "https://github.com/breezy-team/patchkit" authors = ["Jelmer Vernooij "] homepage = "https://github.com/breezy-team/patchkit" [dependencies] chrono = "0.4" lazy-regex = ">=2" lazy_static = "1" once_cell = "1.19.0" regex = "1" patchkit-0.1.8/README.md000064400000000000000000000003031046102023000127140ustar 00000000000000Parsing and manipulation of patch files --------------------------------------- This crate provides support for parsing and editing of unified diff files, as well as related files (e.g. quilt). patchkit-0.1.8/TODO000064400000000000000000000002001046102023000121210ustar 00000000000000- support applying patches with fuzz - support generating diffs + myers + patiencediff + stone - support generating rej/orig patchkit-0.1.8/disperse.conf000064400000000000000000000000461046102023000141260ustar 00000000000000timeout_days: 5 tag_name: "v$VERSION" patchkit-0.1.8/src/ed.rs000064400000000000000000000137671046102023000132040ustar 00000000000000#[derive(Clone, Debug, PartialEq, Eq)] pub struct EdPatch { pub hunks: Vec, } impl EdPatch { pub fn apply(&self, data: &[Vec]) -> Result, Vec> { let mut data = data.to_vec(); for hunk in &self.hunks { match hunk { EdHunk::Remove(start, end, expected) | EdHunk::Change(start, end, expected, _) => { let existing = match data.get(start - 1) { Some(existing) => existing, None => return Err(b"".to_vec()), }; if existing != expected { return Err(existing.to_vec()); } data.remove(start - 1); } _ => {} } match hunk { EdHunk::Add(start, end, added) | EdHunk::Change(start, end, _, added) => { data.insert(start - 1, added.to_vec()); } _ => {} } } Ok(data.concat()) } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum EdHunk { Add(usize, usize, Vec), Remove(usize, usize, Vec), Change(usize, usize, Vec, Vec), } pub fn parse_hunk_header(line: &[u8]) -> Option<(char, usize, usize)> { let cap = lazy_regex::BytesRegex::new("(\\d+)([adc])(\\d+)\n") .unwrap() .captures(line)?; let start = std::str::from_utf8(cap.get(1).unwrap().as_bytes()) .ok()? .parse() .ok()?; let cmd = std::str::from_utf8(cap.get(2).unwrap().as_bytes()) .ok()? .chars() .next()?; let end = std::str::from_utf8(cap.get(3).unwrap().as_bytes()) .ok()? .parse() .ok()?; Some((cmd, start, end)) } #[cfg(test)] mod parse_hunk_header_tests { use super::*; #[test] fn test_parse_hunk_header() { assert_eq!(parse_hunk_header(b"5a10\n"), Some(('a', 5, 10))); assert_eq!(parse_hunk_header(b"5d10\n"), Some(('d', 5, 10))); assert_eq!(parse_hunk_header(b"5c10\n"), Some(('c', 5, 10))); assert_eq!(parse_hunk_header(b"5a\n"), None); assert_eq!(parse_hunk_header(b"a10\n"), None); assert_eq!(parse_hunk_header(b"5\n"), None); assert_eq!(parse_hunk_header(b"a\n"), None); assert_eq!(parse_hunk_header(b"\n"), None); } } pub fn parse_hunk_line<'a>(prefix: &[u8], line: &'a [u8]) -> Option<&'a [u8]> { if line.starts_with(prefix) { Some(&line[prefix.len()..]) } else { None } } impl EdPatch { pub fn parse_patch(patch: &[u8]) -> Result> { let mut hunks = Vec::new(); let mut lines = crate::parse::splitlines(patch); while let Some(line) = lines.next() { if line.is_empty() { continue; } let (cmd, start, end) = match parse_hunk_header(line) { Some((cmd, start, end)) => (cmd, start, end), None => return Err(line.to_vec()), }; let hunk = match cmd { 'a' => { let line = lines.next().ok_or_else(|| line.to_vec())?; let data = parse_hunk_line(b"> ", line).ok_or_else(|| line.to_vec())?; EdHunk::Add(start, end, data.to_vec()) } 'd' => { let line = lines.next().ok_or_else(|| line.to_vec())?; let data = parse_hunk_line(b"< ", line).ok_or_else(|| line.to_vec())?; EdHunk::Remove(start, end, data.to_vec()) } 'c' => { let line = lines.next().ok_or_else(|| line.to_vec())?; let data = parse_hunk_line(b"< ", line).ok_or_else(|| line.to_vec())?; if let Some(line) = lines.next() { if line != b"---\n" { return Err(line.to_vec()); } } else { return Err(line.to_vec()); } let line = lines.next().ok_or_else(|| line.to_vec())?; let data2 = parse_hunk_line(b"> ", line).ok_or_else(|| line.to_vec())?; EdHunk::Change(start, end, data.to_vec(), data2.to_vec()) } _ => return Err(line.to_vec()), }; hunks.push(hunk) } Ok(EdPatch { hunks }) } } #[cfg(test)] mod apply_patch_tests { use super::*; #[test] fn test_apply_add() { let patch = EdPatch { hunks: vec![EdHunk::Add(1, 1, b"hello\n".to_vec())], }; let data = vec![b"world\n".to_vec()]; assert_eq!(patch.apply(&data).unwrap(), b"hello\nworld\n".to_vec()); } #[test] fn test_apply_remove() { let patch = EdPatch { hunks: vec![EdHunk::Remove(2, 2, b"world\n".to_vec())], }; let data = vec![b"hello\n".to_vec(), b"world\n".to_vec()]; assert_eq!(patch.apply(&data).unwrap(), b"hello\n".to_vec()); } #[test] fn test_apply_change() { let patch = EdPatch { hunks: vec![EdHunk::Change( 2, 2, b"world\n".to_vec(), b"hello\n".to_vec(), )], }; let data = vec![b"hello\n".to_vec(), b"world\n".to_vec()]; assert_eq!(patch.apply(&data).unwrap(), b"hello\nhello\n".to_vec()); } } #[cfg(test)] mod parse_patch_tests { use super::*; #[test] fn test_parse_patch() { let patch = b"5a10 > hello 5d10 < hello 5c10 < hello --- > hello "; let patch = EdPatch::parse_patch(patch).unwrap(); assert_eq!( patch, EdPatch { hunks: vec![ EdHunk::Add(5, 10, b"hello\n".to_vec()), EdHunk::Remove(5, 10, b"hello\n".to_vec()), EdHunk::Change(5, 10, b"hello\n".to_vec(), b"hello\n".to_vec()), ] } ); } } patchkit-0.1.8/src/lib.rs000064400000000000000000000032321046102023000133440ustar 00000000000000//! A crate for parsing and manipulating patches. //! //! # Examples //! //! ``` //! use patchkit::parse::parse_patch; //! use patchkit::patch::{Patch as _, UnifiedPatch, Hunk, HunkLine}; //! //! let patch = UnifiedPatch::parse_patch(vec![ //! "--- a/file1\n", //! "+++ b/file1\n", //! "@@ -1,1 +1,1 @@\n", //! "-a\n", //! "+b\n", //! ].into_iter().map(|s| s.as_bytes())).unwrap(); //! //! assert_eq!(patch, UnifiedPatch { //! orig_name: b"a/file1".to_vec(), //! mod_name: b"b/file1".to_vec(), //! orig_ts: None, //! mod_ts: None, //! hunks: vec![ //! Hunk { //! mod_pos: 1, //! mod_range: 1, //! orig_pos: 1, //! orig_range: 1, //! lines: vec![ //! HunkLine::RemoveLine(b"a\n".to_vec()), //! HunkLine::InsertLine(b"b\n".to_vec()), //! ], //! tail: None //! }, //! ], //! }); //! //! let applied = patch.apply_exact(&b"a\n"[..]).unwrap(); //! assert_eq!(applied, b"b\n"); //! ``` pub mod ed; pub mod parse; pub mod patch; pub mod quilt; pub mod timestamp; // TODO: Return a Path instead of a PathBuf pub fn strip_prefix(path: &std::path::Path, prefix: usize) -> std::path::PathBuf { path.components().skip(prefix).collect() } #[test] fn test_strip_prefix() { assert_eq!( std::path::PathBuf::from("b"), strip_prefix(std::path::Path::new("a/b"), 1) ); assert_eq!( std::path::PathBuf::from("a/b"), strip_prefix(std::path::Path::new("a/b"), 0) ); assert_eq!( std::path::PathBuf::from(""), strip_prefix(std::path::Path::new("a/b"), 2) ); } patchkit-0.1.8/src/parse.rs000064400000000000000000000706231046102023000137200ustar 00000000000000use crate::patch::{BinaryPatch, Hunk, HunkLine, Patch, UnifiedPatch}; #[derive(Debug, PartialEq, Eq)] pub enum Error { BinaryFiles(Vec, Vec), PatchSyntax(&'static str, Vec), MalformedPatchHeader(&'static str, Vec), MalformedHunkHeader(String, Vec), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::BinaryFiles(oldname, newname) => { write!(f, "Binary files {:?} and {:?} differ", oldname, newname) } Self::PatchSyntax(msg, line) => write!(f, "Patch syntax error: {} in {:?}", msg, line), Self::MalformedPatchHeader(msg, line) => { write!(f, "Malformed patch header: {} in {:?}", msg, line) } Self::MalformedHunkHeader(msg, line) => { write!(f, "Malformed hunk header: {} in {:?}", msg, line) } } } } impl std::error::Error for Error {} /// Split lines but preserve trailing newlines pub fn splitlines(data: &[u8]) -> impl Iterator { let mut start = 0; let mut end = 0; std::iter::from_fn(move || loop { if end == data.len() { if start == end { return None; } let line = &data[start..end]; start = end; return Some(line); } let c = data[end]; end += 1; if c == b'\n' { let line = &data[start..end]; start = end; return Some(line); } }) } #[cfg(test)] mod splitlines_tests { #[test] fn test_simple() { let data = b"line 1\nline 2\nline 3\n"; let lines: Vec<&[u8]> = super::splitlines(data).collect(); assert_eq!( lines, vec![ "line 1\n".as_bytes(), "line 2\n".as_bytes(), "line 3\n".as_bytes() ] ); } #[test] fn test_no_trailing() { let data = b"line 1\nline 2\nline 3"; let lines: Vec<&[u8]> = super::splitlines(data).collect(); assert_eq!( lines, vec![&b"line 1\n"[..], &b"line 2\n"[..], &b"line 3"[..]] ); } #[test] fn test_empty_line() { let data = b"line 1\n\nline 3\n"; let lines: Vec<&[u8]> = super::splitlines(data).collect(); assert_eq!(lines, vec![&b"line 1\n"[..], &b"\n"[..], &b"line 3\n"[..]]); } } pub const NO_NL: &[u8] = b"\\ No newline at end of file\n"; /// Iterate through a series of lines, ensuring that lines /// that originally had no terminating newline are produced /// without one. pub fn iter_lines_handle_nl<'a, I>(mut iter_lines: I) -> impl Iterator + 'a where I: Iterator + 'a, { let mut last_line: Option<&'a [u8]> = None; std::iter::from_fn(move || { for line in iter_lines.by_ref() { if line == NO_NL { if let Some(last) = last_line.as_mut() { assert!(last.ends_with(b"\n")); // Drop the last newline from `last` *last = &last[..last.len() - 1]; } else { panic!("No newline indicator without previous line"); } } else { if let Some(last) = last_line.take() { last_line = Some(line); return Some(last); } last_line = Some(line); } } last_line.take() }) } #[test] fn test_iter_lines_handle_nl() { let lines = vec![ &b"line 1\n"[..], &b"line 2\n"[..], &b"line 3\n"[..], &b"line 4\n"[..], &b"\\ No newline at end of file\n"[..], ]; let mut iter = iter_lines_handle_nl(lines.into_iter()); assert_eq!(iter.next(), Some("line 1\n".as_bytes())); assert_eq!(iter.next(), Some("line 2\n".as_bytes())); assert_eq!(iter.next(), Some("line 3\n".as_bytes())); assert_eq!(iter.next(), Some("line 4".as_bytes())); assert_eq!(iter.next(), None); } static BINARY_FILES_RE: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { lazy_regex::BytesRegex::new(r"^Binary files (.+) and (.+) differ").unwrap() }); pub fn get_patch_names<'a, T: Iterator>( iter_lines: &mut T, ) -> Result<((Vec, Option>), (Vec, Option>)), Error> { let line = iter_lines .next() .ok_or_else(|| Error::PatchSyntax("No input", vec![]))?; if let Some(captures) = BINARY_FILES_RE.captures(line) { let orig_name = captures.get(1).unwrap().as_bytes().to_vec(); let mod_name = captures.get(2).unwrap().as_bytes().to_vec(); return Err(Error::BinaryFiles(orig_name, mod_name)); } let orig_name = line .strip_prefix(b"--- ") .ok_or_else(|| Error::MalformedPatchHeader("No orig name", line.to_vec()))? .strip_suffix(b"\n") .ok_or_else(|| Error::PatchSyntax("missing newline", line.to_vec()))?; let (orig_name, orig_ts) = match orig_name.split(|&c| c == b'\t').collect::>()[..] { [name, ts] => (name.to_vec(), Some(ts.to_vec())), [name] => (name.to_vec(), None), _ => return Err(Error::MalformedPatchHeader("No orig line", line.to_vec())), }; let line = iter_lines .next() .ok_or_else(|| Error::PatchSyntax("No input", vec![]))?; let (mod_name, mod_ts) = match line.strip_prefix(b"+++ ") { Some(line) => { let mod_name = line .strip_suffix(b"\n") .ok_or_else(|| Error::PatchSyntax("missing newline", line.to_vec()))?; let (mod_name, mod_ts) = match mod_name.split(|&c| c == b'\t').collect::>()[..] { [name, ts] => (name.to_vec(), Some(ts.to_vec())), [name] => (name.to_vec(), None), _ => return Err(Error::PatchSyntax("Invalid mod name", line.to_vec())), }; (mod_name, mod_ts) } None => return Err(Error::MalformedPatchHeader("No mod line", line.to_vec())), }; Ok(((orig_name, orig_ts), (mod_name, mod_ts))) } #[cfg(test)] mod get_patch_names_tests { #[test] fn test_simple() { let lines = [ &b"--- baz 2009-10-14 19:49:59 +0000\n"[..], &b"+++ quxx 2009-10-14 19:51:00 +0000\n"[..], ]; let mut iter = lines.into_iter(); let (old, new) = super::get_patch_names(&mut iter).unwrap(); assert_eq!( old, (b"baz".to_vec(), Some(b"2009-10-14 19:49:59 +0000".to_vec())) ); assert_eq!( new, ( b"quxx".to_vec(), Some(b"2009-10-14 19:51:00 +0000".to_vec()) ) ); } #[test] fn test_binary() { let lines = [&b"Binary files qoo and bar differ\n"[..]]; let mut iter = lines.into_iter(); let e = super::get_patch_names(&mut iter).unwrap_err(); assert_eq!( e, super::Error::BinaryFiles(b"qoo".to_vec(), b"bar".to_vec()) ); } } pub fn iter_hunks<'a, I>(iter_lines: &mut I) -> impl Iterator> + '_ where I: Iterator, { std::iter::from_fn(move || { while let Some(line) = iter_lines.next() { if line == b"\n" { continue; } match Hunk::from_header(line) { Ok(mut new_hunk) => { let mut orig_size = 0; let mut mod_size = 0; while orig_size < new_hunk.orig_range || mod_size < new_hunk.mod_range { let line = iter_lines.next()?; match HunkLine::parse_line(line) { Err(_) => { return Some(Err(Error::PatchSyntax( "Invalid hunk line", line.to_vec(), ))); } Ok(hunk_line) => { if matches!( hunk_line, HunkLine::RemoveLine(_) | HunkLine::ContextLine(_) ) { orig_size += 1 } if matches!( hunk_line, HunkLine::InsertLine(_) | HunkLine::ContextLine(_) ) { mod_size += 1 } new_hunk.lines.push(hunk_line); } } } return Some(Ok(new_hunk)); } Err(crate::patch::MalformedHunkHeader(m, l)) => { return Some(Err(Error::MalformedHunkHeader(m.to_string(), l))); } } } None }) } #[cfg(test)] mod iter_hunks_tests { use super::{Hunk, HunkLine}; #[test] fn test_iter_hunks() { let mut lines = super::splitlines( br#"@@ -391,6 +391,8 @@ else: assert isinstance(hunk_line, RemoveLine) line_no += 1 + for line in orig_lines: + yield line import unittest import os.path "#, ); let hunks = super::iter_hunks(&mut lines) .collect::, crate::parse::Error>>() .unwrap(); let mut expected_hunk = Hunk::new(391, 6, 391, 8, None); expected_hunk.lines.extend([ HunkLine::ContextLine(b" else:\n".to_vec()), HunkLine::ContextLine( b" assert isinstance(hunk_line, RemoveLine)\n".to_vec(), ), HunkLine::ContextLine(b" line_no += 1\n".to_vec()), HunkLine::InsertLine(b" for line in orig_lines:\n".to_vec()), HunkLine::InsertLine(b" yield line\n".to_vec()), HunkLine::ContextLine(b" \n".to_vec()), HunkLine::ContextLine(b"import unittest\n".to_vec()), HunkLine::ContextLine(b"import os.path\n".to_vec()), ]); assert_eq!(&expected_hunk, hunks.first().unwrap()); } } pub fn parse_patch<'a, I>(iter_lines: I) -> Result, Error> where I: Iterator + 'a, { let mut iter_lines = iter_lines_handle_nl(iter_lines); let ((orig_name, orig_ts), (mod_name, mod_ts)) = match get_patch_names(&mut iter_lines) { Ok(names) => names, Err(Error::BinaryFiles(orig_name, mod_name)) => { return Ok(Box::new(BinaryPatch(orig_name, mod_name))); } Err(e) => return Err(e), }; let mut patch = UnifiedPatch::new(orig_name, orig_ts, mod_name, mod_ts); for hunk in iter_hunks(&mut iter_lines) { patch.hunks.push(hunk?); } Ok(Box::new(patch)) } #[cfg(test)] mod patches_tests { macro_rules! test_patch { ($name:ident, $orig:expr, $mod:expr, $patch:expr) => { #[test] fn $name() { let orig = include_bytes!(concat!("../test_patches_data/", $orig)); let modi = include_bytes!(concat!("../test_patches_data/", $mod)); let patch = include_bytes!(concat!("../test_patches_data/", $patch)); let parsed = super::parse_patch(super::splitlines(patch)).unwrap(); let mut patched = Vec::new(); let mut iter = parsed.apply_exact(orig).unwrap().into_iter(); while let Some(line) = iter.next() { patched.push(line); } assert_eq!(patched, modi); } }; } test_patch!(test_patch_2, "orig-2", "mod-2", "diff-2"); test_patch!(test_patch_3, "orig-3", "mod-3", "diff-3"); test_patch!(test_patch_4, "orig-4", "mod-4", "diff-4"); test_patch!(test_patch_5, "orig-5", "mod-5", "diff-5"); test_patch!(test_patch_6, "orig-6", "mod-6", "diff-6"); test_patch!(test_patch_7, "orig-7", "mod-7", "diff-7"); } #[derive(Debug)] pub struct PatchConflict { line_no: usize, orig_line: Vec, patch_line: Vec, } impl std::fmt::Display for PatchConflict { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "Patch conflict at orig line {}: orig: {:?}, patch: {:?}", self.line_no, String::from_utf8_lossy(&self.orig_line), String::from_utf8_lossy(&self.patch_line) ) } } impl std::error::Error for PatchConflict {} struct PatchedIter, L: Iterator>> { orig_lines: L, hunk_lines: Vec, hunks: std::iter::Peekable, line_no: usize, } impl, L: Iterator>> Iterator for PatchedIter { type Item = Result, PatchConflict>; fn next(&mut self) -> Option, PatchConflict>> { loop { // First, check if we just need to yield the next line from the original file. match self.hunks.peek_mut() { // We're ahead of the next hunk. Yield the next line from the original file. Some(hunk) if self.line_no < hunk.orig_pos => { self.line_no += 1; if let Some(line) = self.orig_lines.next() { return Some(Ok(line)); } else { return Some(Err(PatchConflict { line_no: self.line_no, orig_line: Vec::new(), patch_line: Vec::new(), })); } } // There are no more hunks. Yield the rest of the original file. None => { if let Some(line) = self.orig_lines.next() { return Some(Ok(line)); } else { return None; } } Some(_hunk) => { // We're in a hunk. Check if we need to yield a line from the hunk. if let Some(line) = self.hunk_lines.pop() { match line { HunkLine::ContextLine(bytes) => { if let Some(orig_line) = self.orig_lines.next() { if orig_line != bytes { return Some(Err(PatchConflict { line_no: self.line_no, orig_line, patch_line: bytes, })); } } else { return Some(Err(PatchConflict { line_no: self.line_no, orig_line: Vec::new(), patch_line: bytes, })); } self.line_no += 1; return Some(Ok(bytes)); } HunkLine::InsertLine(bytes) => { return Some(Ok(bytes)); } HunkLine::RemoveLine(bytes) => { if let Some(orig_line) = self.orig_lines.next() { if orig_line != bytes { return Some(Err(PatchConflict { line_no: self.line_no, orig_line, patch_line: bytes, })); } } else { return Some(Err(PatchConflict { line_no: self.line_no, orig_line: Vec::new(), patch_line: bytes, })); } self.line_no += 1; } } } else { self.hunks.next(); if let Some(h) = self.hunks.peek_mut() { let mut hunk_lines = h.lines.drain(..).collect::>(); hunk_lines.reverse(); self.hunk_lines = hunk_lines; } } } } } } } #[cfg(test)] mod iter_exact_patched_from_hunks_tests { #[test] fn test_just_context() { let orig_lines = vec![ b"line 1\n".to_vec(), b"line 2\n".to_vec(), b"line 3\n".to_vec(), b"line 4\n".to_vec(), ]; let mut hunk = crate::patch::Hunk::new(1, 1, 1, 1, None); hunk.lines .push(crate::patch::HunkLine::ContextLine(b"line 1\n".to_vec())); let hunks = vec![hunk]; let result = super::iter_exact_patched_from_hunks(orig_lines.into_iter(), hunks.into_iter()) .collect::, _>>() .unwrap(); assert_eq!( &result, &[ b"line 1\n".to_vec(), b"line 2\n".to_vec(), b"line 3\n".to_vec(), b"line 4\n".to_vec(), ] ); } #[test] fn test_insert() { let orig_lines = vec![ b"line 1\n".to_vec(), b"line 2\n".to_vec(), b"line 3\n".to_vec(), b"line 4\n".to_vec(), ]; let mut hunk = crate::patch::Hunk::new(1, 0, 1, 1, None); hunk.lines .push(crate::patch::HunkLine::InsertLine(b"line 0\n".to_vec())); hunk.lines .push(crate::patch::HunkLine::ContextLine(b"line 1\n".to_vec())); let hunks = vec![hunk]; let result = super::iter_exact_patched_from_hunks(orig_lines.into_iter(), hunks.into_iter()) .collect::, _>>() .unwrap(); assert_eq!( &result, &[ b"line 0\n".to_vec(), b"line 1\n".to_vec(), b"line 2\n".to_vec(), b"line 3\n".to_vec(), b"line 4\n".to_vec(), ] ); } } /// Iterate through a series of lines with a patch applied. /// /// This handles a single file, and does exact, not fuzzy patching. /// /// Args: /// orig_lines: The original lines of the file. /// hunks: The hunks to apply to the file. pub fn iter_exact_patched_from_hunks<'a>( orig_lines: impl Iterator> + 'a, hunks: impl Iterator, ) -> impl Iterator, PatchConflict>> { let mut hunks = hunks.peekable(); let mut hunk_lines = if let Some(h) = hunks.peek_mut() { h.lines.drain(..).collect() } else { Vec::new() }; hunk_lines.reverse(); PatchedIter { orig_lines, hunks, line_no: 1, hunk_lines, } } /// Find the index of the first character that differs between two texts pub fn difference_index(atext: &[u8], btext: &[u8]) -> Option { let length = atext.len().min(btext.len()); (0..length).find(|&i| atext[i] != btext[i]) } #[derive(PartialEq, Eq)] pub enum FileEntry { Junk(Vec>), Meta(Vec), Patch(Vec>), } impl std::fmt::Debug for FileEntry { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Junk(lines) => { write!(f, "Junk[")?; // Print the lines interspersed with commas for (i, line) in lines.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{:?}", String::from_utf8_lossy(line))?; } write!(f, "]")?; Ok(()) } Self::Meta(line) => write!(f, "Meta({:?})", String::from_utf8_lossy(line)), Self::Patch(lines) => { write!(f, "Patch[")?; // Print the lines interspersed with commas for (i, line) in lines.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{:?}", String::from_utf8_lossy(line))?; } write!(f, "]")?; Ok(()) } } } } struct FileEntryIter { iter: I, saved_lines: Vec>, is_dirty: bool, orig_range: usize, mod_range: usize, } impl FileEntryIter where I: Iterator>, { fn entry(&mut self) -> Option { if !self.saved_lines.is_empty() { let lines = self.saved_lines.drain(..).collect(); if self.is_dirty { Some(FileEntry::Junk(lines)) } else { Some(FileEntry::Patch(lines)) } } else { None } } } impl Iterator for FileEntryIter where I: Iterator>, { type Item = Result; fn next(&mut self) -> Option> { loop { let line = match self.iter.next() { Some(line) => line, None => { if let Some(entry) = self.entry() { return Some(Ok(entry)); } else { return None; } } }; if line.starts_with(b"=== ") { return Some(Ok(FileEntry::Meta(line))); } else if line.starts_with(b"*** ") { continue; } else if line.starts_with(b"#") { continue; } else if self.orig_range > 0 || self.mod_range > 0 { if line.starts_with(b"-") || line.starts_with(b" ") { self.orig_range -= 1; } if line.starts_with(b"+") || line.starts_with(b" ") { self.mod_range -= 1; } self.saved_lines.push(line); } else if line.starts_with(b"--- ") || BINARY_FILES_RE.is_match(line.as_slice()) { let entry = self.entry(); self.is_dirty = false; self.saved_lines.push(line); if let Some(entry) = entry { return Some(Ok(entry)); } } else if line.starts_with(b"+++ ") && !self.is_dirty { self.saved_lines.push(line); } else if line.starts_with(b"@@") { let hunk = match Hunk::from_header(line.as_slice()) { Ok(hunk) => hunk, Err(e) => { return Some(Err(Error::MalformedHunkHeader(e.to_string(), line.clone()))); } }; self.orig_range = hunk.orig_range; self.mod_range = hunk.mod_range; self.saved_lines.push(line); } else { let entry = if !self.is_dirty { self.entry() } else { None }; self.saved_lines.push(line); self.is_dirty = true; if let Some(entry) = entry { return Some(Ok(entry)); } } } } } /// Iterate through a series of lines. /// /// # Arguments /// * `orig` - The original lines of the file. pub fn iter_file_patch(orig: I) -> impl Iterator> where I: Iterator>, { FileEntryIter { iter: orig, orig_range: 0, saved_lines: Vec::new(), is_dirty: false, mod_range: 0, } } #[cfg(test)] mod iter_file_patch_tests { #[test] fn test_simple() { let lines = [ "--- orig-3 2005-09-23 16:23:20.000000000 -0500\n", "+++ mod-3 2005-09-23 16:23:38.000000000 -0500\n", "@@ -1,3 +1,4 @@\n", "+First line change\n", " # Copyright (C) 2004, 2005 Aaron Bentley\n", " # \n", " #\n", ]; let iter = super::iter_file_patch(lines.into_iter().map(|l| l.as_bytes().to_vec())); let entries = iter.collect::, _>>().unwrap(); assert_eq!( entries, vec![super::FileEntry::Patch( lines .iter() .map(|l| l.as_bytes().to_vec()) .collect::>() )] ); } #[test] fn test_noise() { let lines = [ "=== modified file 'test.txt'\n", "--- orig-3 2005-09-23 16:23:20.000000000 -0500\n", "+++ mod-3 2005-09-23 16:23:38.000000000 -0500\n", "@@ -1,3 +1,4 @@\n", "+First line change\n", " # Copyright (C) 2004, 2005 Aaron Bentley\n", " # \n", " #\n", ]; let iter = super::iter_file_patch(lines.into_iter().map(|l| l.as_bytes().to_vec())); let entries = iter.collect::, _>>().unwrap(); assert_eq!( entries, vec![ super::FileEntry::Meta(lines[0].as_bytes().to_vec()), super::FileEntry::Patch( lines .iter() .skip(1) .map(|l| l.as_bytes().to_vec()) .collect::>() ) ] ); } #[test] fn test_allow_dirty() { let lines = [ "Foo bar\n", "Bar blah\n", "--- orig-3 2005-09-23 16:23:20.000000000 -0500\n", "+++ mod-3 2005-09-23 16:23:38.000000000 -0500\n", "@@ -1,3 +1,4 @@\n", "+First line change\n", " # Copyright (C) 2004, 2005 Aaron Bentley\n", " # \n", " #\n", ]; let iter = super::iter_file_patch(lines.into_iter().map(|l| l.as_bytes().to_vec())); let entries = iter.collect::, _>>().unwrap(); assert_eq!( entries, vec![ super::FileEntry::Junk( lines .iter() .take(2) .map(|l| l.as_bytes().to_vec()) .collect::>() ), super::FileEntry::Patch( lines .iter() .skip(2) .map(|l| l.as_bytes().to_vec()) .collect::>() ) ] ); } } /// Parse a patch file /// /// # Arguments /// * `iter`: Iterator over lines pub fn parse_patches<'a, I>(iter: I) -> Result>, Error> where I: Iterator>, { iter_file_patch(iter) .filter_map(|entry| match entry { Ok(FileEntry::Patch(lines)) => match parse_patch(lines.iter().map(|l| l.as_slice())) { Ok(patch) => Some(Ok(patch)), Err(e) => Some(Err(e)), }, Ok(FileEntry::Junk(_)) => None, Ok(FileEntry::Meta(_)) => None, Err(e) => Some(Err(e)), }) .collect() } #[cfg(test)] mod parse_patches_tests { #[test] fn test_simple() { let lines = [ "--- orig-3 2005-09-23 16:23:20.000000000 -0500\n", "+++ mod-3 2005-09-23 16:23:38.000000000 -0500\n", "@@ -1,3 +1,4 @@\n", "+First line change\n", " # Copyright (C) 2004, 2005 Aaron Bentley\n", " # \n", " #\n", ]; let patches = super::parse_patches(lines.iter().map(|l| l.as_bytes().to_vec())).unwrap(); assert_eq!(patches.len(), 1); } } patchkit-0.1.8/src/patch.rs000064400000000000000000000376431046102023000137120ustar 00000000000000use regex::bytes::Regex; use std::num::ParseIntError; #[derive(Debug)] pub enum ApplyError { Conflict(String), Unapplyable, } impl std::fmt::Display for ApplyError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Conflict(reason) => write!(f, "Conflict: {}", reason), Self::Unapplyable => write!(f, "Patch unapplyable"), } } } impl std::error::Error for ApplyError {} /// A patch of some sort pub trait Patch { /// Old file name fn oldname(&self) -> &[u8]; /// New file name fn newname(&self) -> &[u8]; fn apply_exact(&self, orig: &[u8]) -> Result, ApplyError>; } /// A binary patch #[derive(Clone, Debug, PartialEq, Eq)] pub struct BinaryPatch(pub Vec, pub Vec); impl Patch for BinaryPatch { fn oldname(&self) -> &[u8] { &self.0 } fn newname(&self) -> &[u8] { &self.1 } fn apply_exact(&self, _orig: &[u8]) -> Result, ApplyError> { Err(ApplyError::Unapplyable) } } /// A unified diff style patch #[derive(Clone, Debug, PartialEq, Eq)] pub struct UnifiedPatch { /// Name of the original file pub orig_name: Vec, /// Timestamp for the original file pub orig_ts: Option>, /// Name of the modified file pub mod_name: Vec, /// Timestamp for the modified file pub mod_ts: Option>, /// List of hunks pub hunks: Vec, } impl UnifiedPatch { pub fn new( orig_name: Vec, orig_ts: Option>, mod_name: Vec, mod_ts: Option>, ) -> Self { Self { orig_name, orig_ts, mod_name, mod_ts, hunks: Vec::new(), } } pub fn as_bytes(&self) -> Vec { let mut bytes = Vec::new(); self.write(&mut bytes).unwrap(); bytes } pub fn write(&self, w: &mut W) -> std::io::Result<()> { w.write_all( &format!( "--- {}{}\n", String::from_utf8_lossy(&self.orig_name), match &self.orig_ts { Some(ts) => format!("\t{}", String::from_utf8_lossy(ts)), None => "".to_string(), } ) .into_bytes(), )?; w.write_all( &format!( "+++ {}{}\n", String::from_utf8_lossy(&self.mod_name), match &self.mod_ts { Some(ts) => format!("\t{}", String::from_utf8_lossy(ts)), None => "".to_string(), } ) .into_bytes(), )?; for hunk in &self.hunks { hunk.write(w)?; } Ok(()) } pub fn parse_patch<'a, I>(iter_lines: I) -> Result where I: Iterator + 'a, { let mut iter_lines = crate::parse::iter_lines_handle_nl(iter_lines); let ((orig_name, orig_ts), (mod_name, mod_ts)) = match crate::parse::get_patch_names(&mut iter_lines) { Ok(names) => names, Err(e) => return Err(e), }; let mut patch = Self::new(orig_name, orig_ts, mod_name, mod_ts); for hunk in crate::parse::iter_hunks(&mut iter_lines) { patch.hunks.push(hunk?); } Ok(patch) } /// Parse a unified patch file /// /// # Arguments /// * `iter`: Iterator over lines pub fn parse_patches<'a, I>(iter: I) -> Result, crate::parse::Error> where I: Iterator>, { crate::parse::iter_file_patch(iter) .filter_map(|entry| match entry { Ok(crate::parse::FileEntry::Patch(lines)) => { match Self::parse_patch(lines.iter().map(|l| l.as_slice())) { Ok(patch) => Some(Ok(patch)), Err(e) => Some(Err(e)), } } Ok(crate::parse::FileEntry::Junk(_)) => None, Ok(crate::parse::FileEntry::Meta(_)) => None, Err(e) => Some(Err(e)), }) .collect() } } impl Patch for UnifiedPatch { fn oldname(&self) -> &[u8] { &self.orig_name } fn newname(&self) -> &[u8] { &self.mod_name } fn apply_exact(&self, orig: &[u8]) -> Result, ApplyError> { let orig_lines = crate::parse::splitlines(orig).map(|l| l.to_vec()); let lines = crate::parse::iter_exact_patched_from_hunks(orig_lines, self.hunks.clone().into_iter()) .collect::>, crate::parse::PatchConflict>>() .map_err(|e| ApplyError::Conflict(e.to_string()))?; Ok(lines.concat()) } } #[cfg(test)] mod patch_tests { #[test] fn test_as_bytes_empty_hunks() { let patch = super::UnifiedPatch { orig_name: b"foo".to_vec(), orig_ts: None, mod_name: b"bar".to_vec(), mod_ts: None, hunks: vec![], }; assert_eq!(patch.as_bytes(), b"--- foo\n+++ bar\n"); } #[test] fn test_as_bytes() { let patch = super::UnifiedPatch { orig_name: b"foo".to_vec(), orig_ts: None, mod_name: b"bar".to_vec(), mod_ts: None, hunks: vec![super::Hunk { orig_pos: 1, orig_range: 1, mod_pos: 2, mod_range: 1, tail: None, lines: vec![super::HunkLine::ContextLine(b"foo\n".to_vec())], }], }; assert_eq!(patch.as_bytes(), b"--- foo\n+++ bar\n@@ -1 +2 @@\n foo\n"); } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum HunkLine { ContextLine(Vec), InsertLine(Vec), RemoveLine(Vec), } impl HunkLine { pub fn char(&self) -> u8 { match self { Self::ContextLine(_) => b' ', Self::InsertLine(_) => b'+', Self::RemoveLine(_) => b'-', } } pub fn contents(&self) -> &[u8] { match self { Self::ContextLine(bytes) => bytes, Self::InsertLine(bytes) => bytes, Self::RemoveLine(bytes) => bytes, } } pub fn as_bytes(&self) -> Vec { let leadchar = self.char(); let contents = self.contents(); let terminator = if !contents.ends_with(&b"\n"[..]) { [b"\n".to_vec(), crate::parse::NO_NL.to_vec()].concat() } else { b"".to_vec() }; [vec![leadchar], contents.to_vec(), terminator].concat() } pub fn parse_line(line: &[u8]) -> Result { if line.starts_with(b"\n") { Ok(Self::ContextLine(line.to_vec())) } else if let Some(line) = line.strip_prefix(b" ") { Ok(Self::ContextLine(line.to_vec())) } else if let Some(line) = line.strip_prefix(b"+") { Ok(Self::InsertLine(line.to_vec())) } else if let Some(line) = line.strip_prefix(b"-") { Ok(Self::RemoveLine(line.to_vec())) } else { Err(MalformedLine(line.to_vec())) } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct MalformedLine(Vec); impl std::fmt::Display for MalformedLine { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "Malformed line: {:?}", self.0) } } impl std::error::Error for MalformedLine {} #[cfg(test)] mod hunkline_tests { use super::HunkLine; use super::MalformedLine; #[test] fn test_parse_line() { assert_eq!( HunkLine::parse_line(&b" foo\n"[..]).unwrap(), HunkLine::ContextLine(b"foo\n".to_vec()) ); assert_eq!( HunkLine::parse_line(&b"-foo\n"[..]).unwrap(), HunkLine::RemoveLine(b"foo\n".to_vec()) ); assert_eq!( HunkLine::parse_line(&b"+foo\n"[..]).unwrap(), HunkLine::InsertLine(b"foo\n".to_vec()) ); assert_eq!( HunkLine::parse_line(&b"\n"[..]).unwrap(), HunkLine::ContextLine(b"\n".to_vec()) ); assert_eq!( HunkLine::parse_line(&b"aaaaa\n"[..]).unwrap_err(), MalformedLine(b"aaaaa\n".to_vec()) ); } #[test] fn as_bytes() { assert_eq!( HunkLine::ContextLine(b"foo\n".to_vec()).as_bytes(), b" foo\n" ); assert_eq!( HunkLine::InsertLine(b"foo\n".to_vec()).as_bytes(), b"+foo\n" ); assert_eq!( HunkLine::RemoveLine(b"foo\n".to_vec()).as_bytes(), b"-foo\n" ); } #[test] fn as_bytes_no_nl() { assert_eq!( HunkLine::ContextLine(b"foo".to_vec()).as_bytes(), b" foo\n\\ No newline at end of file\n" ); } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct MalformedHunkHeader(pub &'static str, pub Vec); impl std::fmt::Display for MalformedHunkHeader { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "Malformed hunk header: {}: {:?}", self.0, self.1) } } impl std::error::Error for MalformedHunkHeader {} #[derive(PartialEq, Eq, Debug, Clone)] pub struct Hunk { pub orig_pos: usize, pub orig_range: usize, pub mod_pos: usize, pub mod_range: usize, pub tail: Option>, pub lines: Vec, } impl Hunk { pub fn new( orig_pos: usize, orig_range: usize, mod_pos: usize, mod_range: usize, tail: Option>, ) -> Self { Self { orig_pos, orig_range, mod_pos, mod_range, tail, lines: Vec::new(), } } pub fn from_header(line: &[u8]) -> Result { let re = Regex::new(r"\@\@ ([^@]*) \@\@( (.*))?\n").unwrap(); let captures = re .captures(line) .ok_or_else(|| MalformedHunkHeader("Does not match format.", line.to_vec()))?; let (orig, modi) = match captures[1].split(|b| *b == b' ').collect::>()[..] { [orig, modi] => Ok((orig, modi)), _ => return Err(MalformedHunkHeader("Does not match format.", line.to_vec())), }?; if orig[0] != b'-' || modi[0] != b'+' { return Err(MalformedHunkHeader( "Positions don't start with + or -.", line.to_vec(), )); } let (orig_pos, orig_range) = parse_range(&String::from_utf8_lossy(&orig[1..])) .map_err(|_| MalformedHunkHeader("Original range is not a number.", line.to_vec()))?; let (mod_pos, mod_range) = parse_range(&String::from_utf8_lossy(modi[1..].as_ref())) .map_err(|_| MalformedHunkHeader("Modified range is not a number.", line.to_vec()))?; let tail = captures.get(3).map(|m| m.as_bytes().to_vec()); Ok(Self::new(orig_pos, orig_range, mod_pos, mod_range, tail)) } pub fn lines(&self) -> &[HunkLine] { &self.lines } pub fn get_header(&self) -> Vec { let tail_str = match &self.tail { Some(tail) => [b" ".to_vec(), tail.to_vec()].concat(), None => Vec::new(), }; format!( "@@ -{} +{} @@{}\n", self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range), String::from_utf8_lossy(&tail_str), ) .into_bytes() } fn range_str(&self, pos: usize, range: usize) -> String { if range == 1 { format!("{}", pos) } else { format!("{},{}", pos, range) } } pub fn write(&self, w: &mut W) -> std::io::Result<()> { w.write_all(&self.get_header())?; for line in &self.lines { w.write_all(&line.as_bytes())?; } Ok(()) } pub fn as_bytes(&self) -> Vec { let mut bytes = Vec::new(); self.write(&mut bytes).unwrap(); bytes } pub fn shift_to_mod(&self, pos: usize) -> Option { if pos < self.orig_pos - 1 { Some(0) } else if pos > self.orig_pos + self.orig_range { Some((self.mod_range as isize) - (self.orig_range as isize)) } else { self.shift_to_mod_lines(pos) } } fn shift_to_mod_lines(&self, pos: usize) -> Option { let mut position = self.orig_pos - 1; let mut shift = 0; for line in &self.lines { match line { HunkLine::InsertLine(_) => shift += 1, HunkLine::RemoveLine(_) => { if position == pos { return None; } shift -= 1; position += 1; } HunkLine::ContextLine(_) => position += 1, } if position > pos { break; } } Some(shift) } } #[cfg(test)] mod hunk_tests { use super::Hunk; #[test] fn from_header_test() { let hunk = Hunk::from_header(&b"@@ -1 +2 @@\n"[..]).unwrap(); assert_eq!(hunk, Hunk::new(1, 1, 2, 1, None)); } #[test] fn from_header_tail() { let hunk = Hunk::from_header(&b"@@ -1 +2 @@ function()\n"[..]).unwrap(); assert_eq!(hunk, Hunk::new(1, 1, 2, 1, Some(b"function()".to_vec()))); } #[test] fn test_valid_hunk_header() { let header = b"@@ -34,11 +50,6 @@\n"; let hunk = Hunk::from_header(&header[..]).unwrap(); assert_eq!(hunk.orig_pos, 34); assert_eq!(hunk.orig_range, 11); assert_eq!(hunk.mod_pos, 50); assert_eq!(hunk.mod_range, 6); assert_eq!(hunk.as_bytes(), &header[..]); } #[test] fn test_valid_hunk_header2() { let header = b"@@ -1 +0,0 @@\n"; let hunk = Hunk::from_header(&header[..]).unwrap(); assert_eq!(hunk.orig_pos, 1); assert_eq!(hunk.orig_range, 1); assert_eq!(hunk.mod_pos, 0); assert_eq!(hunk.mod_range, 0); assert_eq!(hunk.as_bytes(), header); } /// Parse a hunk header produced by diff -p. #[test] fn test_pdiff() { let header = b"@@ -407,7 +292,7 @@ bzr 0.18rc1 2007-07-10\n"; let hunk = Hunk::from_header(header).unwrap(); assert_eq!(&b"bzr 0.18rc1 2007-07-10"[..], hunk.tail.as_ref().unwrap()); assert_eq!(&header[..], hunk.as_bytes()); } fn assert_malformed_header(header: &[u8]) { let err = Hunk::from_header(header).unwrap_err(); assert!(matches!(err, super::MalformedHunkHeader(..))); } #[test] fn test_invalid_header() { assert_malformed_header(&b" -34,11 +50,6 \n"[..]); assert_malformed_header(&b"@@ +50,6 -34,11 @@\n"[..]); assert_malformed_header(&b"@@ -34,11 +50,6 @@"[..]); assert_malformed_header(&b"@@ -34.5,11 +50,6 @@\n"[..]); assert_malformed_header(&b"@@-34,11 +50,6@@\n"[..]); assert_malformed_header(&b"@@ 34,11 50,6 @@\n"[..]); assert_malformed_header(&b"@@ -34,11 @@\n"[..]); assert_malformed_header(&b"@@ -34,11 +50,6.5 @@\n"[..]); assert_malformed_header(&b"@@ -34,11 +50,-6 @@\n"[..]); } } /// Parse a patch range, handling the "1" special-case pub fn parse_range(textrange: &str) -> Result<(usize, usize), ParseIntError> { let tmp: Vec<&str> = textrange.split(',').collect(); let (pos, brange) = if tmp.len() == 1 { (tmp[0], "1") } else { (tmp[0], tmp[1]) }; let pos = pos.parse::()?; let range = brange.parse::()?; Ok((pos, range)) } #[cfg(test)] mod parse_range_tests { use super::parse_range; #[test] fn parse_range_test() { assert_eq!((2, 1), parse_range("2").unwrap()); assert_eq!((2, 1), parse_range("2,1").unwrap()); parse_range("foo").unwrap_err(); } } patchkit-0.1.8/src/quilt.rs000064400000000000000000000125461046102023000137440ustar 00000000000000use std::collections::HashMap; use std::io::BufRead; pub const DEFAULT_PATCHES_DIR: &str = "patches"; pub const DEFAULT_SERIES_FILE: &str = "series"; /// Find the common prefix to use for patches /// /// # Arguments /// * `names` - An iterator of patch names /// /// # Returns /// The common prefix, or `None` if there is no common prefix pub fn find_common_patch_suffix<'a>(names: impl Iterator) -> Option<&'a str> { let mut suffix_count = HashMap::new(); for name in names { if name == "series" || name == "00list" { continue; } if name.starts_with("README") { continue; } let suffix = name.find('.').map(|index| &name[index..]).unwrap_or(""); suffix_count .entry(suffix) .and_modify(|count| *count += 1) .or_insert(1); } // Just find the suffix with the highest count and return it suffix_count .into_iter() .max_by_key(|(_, count)| *count) .map(|(suffix, _)| suffix) } #[cfg(test)] mod find_common_patch_suffix_tests { #[test] fn test_find_common_patch_suffix() { let names = vec![ "0001-foo.patch", "0002-bar.patch", "0003-baz.patch", "0004-qux.patch", ]; assert_eq!( super::find_common_patch_suffix(names.into_iter()), Some(".patch") ); } #[test] fn test_find_common_patch_suffix_no_common_suffix() { let names = vec!["0001-foo.patch", "0002-bar.patch", "0003-baz.patch", "0004-qux"]; assert_eq!(super::find_common_patch_suffix(names.into_iter()), Some(".patch")); } #[test] fn test_find_common_patch_suffix_no_patches() { let names = vec!["README", "0001-foo.patch", "0002-bar.patch", "0003-baz.patch"]; assert_eq!(super::find_common_patch_suffix(names.into_iter()), Some(".patch")); } } #[derive(Debug)] pub enum SeriesEntry { Patch { name: String, options: Vec, }, Comment(String), } /// A quilt series file #[derive(Debug)] pub struct Series { pub entries: Vec, } impl Series { pub fn new() -> Self { Self { entries: vec![] } } pub fn len(&self) -> usize { self.entries.iter().filter(|entry| matches!(entry, SeriesEntry::Patch { .. })).count() } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn contains(&self, name: &str) -> bool { self.entries.iter().any(|entry| match entry { SeriesEntry::Patch { name: entry_name, .. } => entry_name == name, _ => false, }) } pub fn read(reader: R) -> std::io::Result { let mut series = Self::new(); let reader = std::io::BufReader::new(reader); for line in reader.lines() { let line = line?; let line = line.trim(); if line.starts_with('#') { series.entries.push(SeriesEntry::Comment(line.to_string())); continue; } let mut parts = line.split_whitespace(); let name = parts.next().ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::InvalidData, "missing patch name in series file", ) })?; let options = parts.map(|s| s.to_string()).collect(); series.entries.push(SeriesEntry::Patch { name: name.to_string(), options }); } Ok(series) } /// Remove a patch from the series file pub fn remove(&mut self, name: &str) { self.entries.retain(|entry| match entry { SeriesEntry::Patch { name: entry_name, .. } => entry_name != name, _ => true, }); } pub fn patches(&self) -> impl Iterator { self.entries.iter().filter_map(|entry| match entry { SeriesEntry::Patch { name, .. } => Some(name.as_str()), _ => None, }) } /// Append a patch to the series file pub fn append(&mut self, name: &str, options: Option<&[String]>) { self.entries.push(SeriesEntry::Patch { name: name.to_string(), options: options.map(|options| options.to_vec()).unwrap_or_default(), }); } pub fn write(&self, writer: &mut W) -> std::io::Result<()> { for entry in &self.entries { match entry { SeriesEntry::Patch { name, options } => { write!(writer, "{}", name)?; for option in options { write!(writer, " {}", option)?; } writeln!(writer)?; } SeriesEntry::Comment(comment) => { writeln!(writer, "# {}", comment)?; } } } Ok(()) } } impl Default for Series { fn default() -> Self { Self::new() } } /// Read a .pc/.quilt_patches file pub fn read_quilt_patches(mut reader: R) -> std::path::PathBuf { let mut p = String::new(); reader.read_to_string(&mut p).unwrap(); p.into() } /// Read a .pc/.quilt_series file pub fn read_quilt_series(mut reader: R) -> std::path::PathBuf { let mut s = String::new(); reader.read_to_string(&mut s).unwrap(); s.into() } patchkit-0.1.8/src/timestamp.rs000064400000000000000000000065541046102023000146130ustar 00000000000000use lazy_static::lazy_static; #[derive(Debug)] pub enum ParsePatchDateError { InvalidDate(String), MissingTimezoneOffset(String), InvalidTimezoneOffset(String), } #[derive(Debug)] pub enum FormatPatchDateError { InvalidTimezoneOffset(i64), NegativeTime(i64, i64), } pub fn format_patch_date(secs: i64, mut offset: i64) -> Result { if offset % 60 != 0 { return Err(FormatPatchDateError::InvalidTimezoneOffset(offset)); } // so that we don't need to do calculations on pre-epoch times, // which doesn't work with win32 python gmtime, we always // give the epoch in utc if secs == 0 { offset = 0; } if secs + offset < 0 { return Err(FormatPatchDateError::NegativeTime(secs, offset)); } let dt = chrono::DateTime::::from_utc(chrono::NaiveDateTime::from_timestamp(secs, 0), chrono::Utc); let sign = if offset >= 0 { '+' } else { '-' }; let hours = offset.abs() / 3600; let minutes = (offset.abs() / 60) % 60; Ok(format!("{} {}{:02}{:02}", dt.format("%Y-%m-%d %H:%M:%S"), sign ,hours, minutes)) } pub fn parse_patch_date(date_str: &str) -> Result<(i64, i64), ParsePatchDateError> { lazy_static! { // Format for patch dates: %Y-%m-%d %H:%M:%S [+-]%H%M // Groups: 1 = %Y-%m-%d %H:%M:%S; 2 = [+-]%H; 3 = %M static ref RE_PATCHDATE: regex::Regex = regex::Regex::new(r"(\d+-\d+-\d+\s+\d+:\d+:\d+)\s*([+-]\d\d)(\d\d)$").unwrap(); static ref RE_PATCHDATE_NOOFFSET: regex:: Regex = regex::Regex::new(r"\d+-\d+-\d+\s+\d+:\d+:\d+$").unwrap(); } let m = RE_PATCHDATE.captures(date_str); if m.is_none() { if RE_PATCHDATE_NOOFFSET.captures(date_str).is_some() { return Err(ParsePatchDateError::MissingTimezoneOffset( date_str.to_string(), )); } else { return Err(ParsePatchDateError::InvalidDate(date_str.to_string())); } } let m = m.unwrap(); let secs_str = m.get(1).unwrap().as_str(); let offset_hours = m .get(2) .unwrap() .as_str() .parse::() .map_err(|_| ParsePatchDateError::InvalidTimezoneOffset(date_str.to_string()))?; let offset_minutes = m .get(3) .unwrap() .as_str() .parse::() .map_err(|_| ParsePatchDateError::InvalidTimezoneOffset(date_str.to_string()))?; if offset_hours.abs() >= 24 || offset_minutes >= 60 { return Err(ParsePatchDateError::InvalidTimezoneOffset( date_str.to_string(), )); } let offset = offset_hours * 3600 + offset_minutes * 60; // Parse secs_str with a time format %Y-%m-%d %H:%M:%S using the chrono crate let dt = chrono::NaiveDateTime::parse_from_str(secs_str, "%Y-%m-%d %H:%M:%S") .map_err(|_| ParsePatchDateError::InvalidDate(date_str.to_string()))? - chrono::Duration::seconds(offset); Ok((dt.timestamp(), offset)) } #[cfg(test)] mod test { #[test] fn test_parse_patch_date() { assert_eq!( super::parse_patch_date("2019-01-01 00:00:00 +0000").unwrap(), (1546300800, 0) ); match super::parse_patch_date("2019-01-01 00:00:00") { Err(super::ParsePatchDateError::MissingTimezoneOffset(_)) => (), e => panic!("Expected MissingTimezoneOffset error, got {:?}", e), } } } patchkit-0.1.8/test_patches_data/binary-after-normal.patch000064400000000000000000000002651046102023000220150ustar 00000000000000--- baz 2009-10-14 19:49:59 +0000 +++ quxx 2009-10-14 19:51:00 +0000 @@ -1 +1 @@ -hello +goodbye Binary files bar 2009-10-14 19:49:59 +0000 and qux 2009-10-14 19:50:35 +0000 differ patchkit-0.1.8/test_patches_data/binary.patch000064400000000000000000000002011046102023000174160ustar 00000000000000Binary files bar and qux differ --- baz 2009-10-14 19:49:59 +0000 +++ quxx 2009-10-14 19:51:00 +0000 @@ -1 +1 @@ -hello +goodbye patchkit-0.1.8/test_patches_data/diff000064400000000000000000001327571046102023000157720ustar 00000000000000--- orig/commands.py +++ mod/commands.py @@ -19,25 +19,31 @@ import arch import arch.util import arch.arch + +import pylon.errors +from pylon.errors import * +from pylon import errors +from pylon import util +from pylon import arch_core +from pylon import arch_compound +from pylon import ancillary +from pylon import misc +from pylon import paths + import abacmds import cmdutil import shutil import os import options -import paths import time import cmd import readline import re import string -import arch_core -from errors import * -import errors import terminal -import ancillary -import misc import email import smtplib +import textwrap __docformat__ = "restructuredtext" __doc__ = "Implementation of user (sub) commands" @@ -257,7 +263,7 @@ tree=arch.tree_root() if len(args) == 0: - a_spec = cmdutil.comp_revision(tree) + a_spec = ancillary.comp_revision(tree) else: a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) @@ -284,7 +290,7 @@ changeset=options.changeset tmpdir = None else: - tmpdir=cmdutil.tmpdir() + tmpdir=util.tmpdir() changeset=tmpdir+"/changeset" try: delta=arch.iter_delta(a_spec, b_spec, changeset) @@ -304,14 +310,14 @@ if status > 1: return if (options.perform_diff): - chan = cmdutil.ChangesetMunger(changeset) + chan = arch_compound.ChangesetMunger(changeset) chan.read_indices() - if isinstance(b_spec, arch.Revision): - b_dir = b_spec.library_find() - else: - b_dir = b_spec - a_dir = a_spec.library_find() if options.diffopts is not None: + if isinstance(b_spec, arch.Revision): + b_dir = b_spec.library_find() + else: + b_dir = b_spec + a_dir = a_spec.library_find() diffopts = options.diffopts.split() cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) else: @@ -517,7 +523,7 @@ except arch.errors.TreeRootError, e: print e return - from_revision=cmdutil.tree_latest(tree) + from_revision = arch_compound.tree_latest(tree) if from_revision==to_revision: print "Tree is already up to date with:\n"+str(to_revision)+"." return @@ -592,6 +598,9 @@ if len(args) == 0: args = None + if options.version is None: + return options, tree.tree_version, args + revision=cmdutil.determine_revision_arch(tree, options.version) return options, revision.get_version(), args @@ -601,11 +610,16 @@ """ tree=arch.tree_root() options, version, files = self.parse_commandline(cmdargs, tree) + ancestor = None if options.__dict__.has_key("base") and options.base: base = cmdutil.determine_revision_tree(tree, options.base) + ancestor = base else: - base = cmdutil.submit_revision(tree) - + base = ancillary.submit_revision(tree) + ancestor = base + if ancestor is None: + ancestor = arch_compound.tree_latest(tree, version) + writeversion=version archive=version.archive source=cmdutil.get_mirror_source(archive) @@ -625,18 +639,26 @@ try: last_revision=tree.iter_logs(version, True).next().revision except StopIteration, e: - if cmdutil.prompt("Import from commit"): - return do_import(version) - else: - raise NoVersionLogs(version) - if last_revision!=version.iter_revisions(True).next(): + last_revision = None + if ancestor is None: + if cmdutil.prompt("Import from commit"): + return do_import(version) + else: + raise NoVersionLogs(version) + try: + arch_last_revision = version.iter_revisions(True).next() + except StopIteration, e: + arch_last_revision = None + + if last_revision != arch_last_revision: + print "Tree is not up to date with %s" % str(version) if not cmdutil.prompt("Out of date"): raise OutOfDate else: allow_old=True try: - if not cmdutil.has_changed(version): + if not cmdutil.has_changed(ancestor): if not cmdutil.prompt("Empty commit"): raise EmptyCommit except arch.util.ExecProblem, e: @@ -645,15 +667,15 @@ raise MissingID(e) else: raise - log = tree.log_message(create=False) + log = tree.log_message(create=False, version=version) if log is None: try: if cmdutil.prompt("Create log"): - edit_log(tree) + edit_log(tree, version) except cmdutil.NoEditorSpecified, e: raise CommandFailed(e) - log = tree.log_message(create=False) + log = tree.log_message(create=False, version=version) if log is None: raise NoLogMessage if log["Summary"] is None or len(log["Summary"].strip()) == 0: @@ -837,23 +859,24 @@ if spec is not None: revision = cmdutil.determine_revision_tree(tree, spec) else: - revision = cmdutil.comp_revision(tree) + revision = ancillary.comp_revision(tree) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) munger = None if options.file_contents or options.file_perms or options.deletions\ or options.additions or options.renames or options.hunk_prompt: - munger = cmdutil.MungeOpts() - munger.hunk_prompt = options.hunk_prompt + munger = arch_compound.MungeOpts() + munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, + options.hunk_prompt) if len(args) > 0 or options.logs or options.pattern_files or \ options.control: if munger is None: - munger = cmdutil.MungeOpts(True) + munger = cmdutil.arch_compound.MungeOpts(True) munger.all_types(True) if len(args) > 0: - t_cwd = cmdutil.tree_cwd(tree) + t_cwd = arch_compound.tree_cwd(tree) for name in args: if len(t_cwd) > 0: t_cwd += "/" @@ -878,7 +901,7 @@ if options.pattern_files: munger.add_keep_pattern(options.pattern_files) - for line in cmdutil.revert(tree, revision, munger, + for line in arch_compound.revert(tree, revision, munger, not options.no_output): cmdutil.colorize(line) @@ -1042,18 +1065,13 @@ help_tree_spec() return -def require_version_exists(version, spec): - if not version.exists(): - raise cmdutil.CantDetermineVersion(spec, - "The version %s does not exist." \ - % version) - class Revisions(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Lists revisions" + self.cl_revisions = [] def do_command(self, cmdargs): """ @@ -1066,224 +1084,68 @@ self.tree = arch.tree_root() except arch.errors.TreeRootError: self.tree = None + if options.type == "default": + options.type = "archive" try: - iter = self.get_iterator(options.type, args, options.reverse, - options.modified) + iter = cmdutil.revision_iterator(self.tree, options.type, args, + options.reverse, options.modified, + options.shallow) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) - + except cmdutil.CantDetermineVersion, e: + raise CommandFailedWrapper(e) if options.skip is not None: iter = cmdutil.iter_skip(iter, int(options.skip)) - for revision in iter: - log = None - if isinstance(revision, arch.Patchlog): - log = revision - revision=revision.revision - print options.display(revision) - if log is None and (options.summary or options.creator or - options.date or options.merges): - log = revision.patchlog - if options.creator: - print " %s" % log.creator - if options.date: - print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) - if options.summary: - print " %s" % log.summary - if options.merges: - showed_title = False - for revision in log.merged_patches: - if not showed_title: - print " Merged:" - showed_title = True - print " %s" % revision - - def get_iterator(self, type, args, reverse, modified): - if len(args) > 0: - spec = args[0] - else: - spec = None - if modified is not None: - iter = cmdutil.modified_iter(modified, self.tree) - if reverse: - return iter - else: - return cmdutil.iter_reverse(iter) - elif type == "archive": - if spec is None: - if self.tree is None: - raise cmdutil.CantDetermineRevision("", - "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - else: - version = cmdutil.determine_version_arch(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - return version.iter_revisions(reverse) - elif type == "cacherevs": - if spec is None: - if self.tree is None: - raise cmdutil.CantDetermineRevision("", - "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - else: - version = cmdutil.determine_version_arch(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - return cmdutil.iter_cacherevs(version, reverse) - elif type == "library": - if spec is None: - if self.tree is None: - raise cmdutil.CantDetermineRevision("", - "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - else: - version = cmdutil.determine_version_arch(spec, self.tree) - return version.iter_library_revisions(reverse) - elif type == "logs": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ - self.tree), reverse) - elif type == "missing" or type == "skip-present": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - skip = (type == "skip-present") - version = cmdutil.determine_version_tree(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - return cmdutil.iter_missing(self.tree, version, reverse, - skip_present=skip) - - elif type == "present": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - return cmdutil.iter_present(self.tree, version, reverse) - - elif type == "new-merges" or type == "direct-merges": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - version = cmdutil.determine_version_tree(spec, self.tree) - cmdutil.ensure_archive_registered(version.archive) - require_version_exists(version, spec) - iter = cmdutil.iter_new_merges(self.tree, version, reverse) - if type == "new-merges": - return iter - elif type == "direct-merges": - return cmdutil.direct_merges(iter) - - elif type == "missing-from": - if self.tree is None: - raise cmdutil.CantDetermineRevision("", "Not in a project tree") - revision = cmdutil.determine_revision_tree(self.tree, spec) - libtree = cmdutil.find_or_make_local_revision(revision) - return cmdutil.iter_missing(libtree, self.tree.tree_version, - reverse) - - elif type == "partner-missing": - return cmdutil.iter_partner_missing(self.tree, reverse) - - elif type == "ancestry": - revision = cmdutil.determine_revision_tree(self.tree, spec) - iter = cmdutil._iter_ancestry(self.tree, revision) - if reverse: - return iter - else: - return cmdutil.iter_reverse(iter) - - elif type == "dependencies" or type == "non-dependencies": - nondeps = (type == "non-dependencies") - revision = cmdutil.determine_revision_tree(self.tree, spec) - anc_iter = cmdutil._iter_ancestry(self.tree, revision) - iter_depends = cmdutil.iter_depends(anc_iter, nondeps) - if reverse: - return iter_depends - else: - return cmdutil.iter_reverse(iter_depends) - elif type == "micro": - return cmdutil.iter_micro(self.tree) - - + try: + for revision in iter: + log = None + if isinstance(revision, arch.Patchlog): + log = revision + revision=revision.revision + out = options.display(revision) + if out is not None: + print out + if log is None and (options.summary or options.creator or + options.date or options.merges): + log = revision.patchlog + if options.creator: + print " %s" % log.creator + if options.date: + print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) + if options.summary: + print " %s" % log.summary + if options.merges: + showed_title = False + for revision in log.merged_patches: + if not showed_title: + print " Merged:" + showed_title = True + print " %s" % revision + if len(self.cl_revisions) > 0: + print pylon.changelog_for_merge(self.cl_revisions) + except pylon.errors.TreeRootNone: + raise CommandFailedWrapper( + Exception("This option can only be used in a project tree.")) + + def changelog_append(self, revision): + if isinstance(revision, arch.Revision): + revision=arch.Patchlog(revision) + self.cl_revisions.append(revision) + def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ - parser=cmdutil.CmdOptionParser("fai revisions [revision]") + parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") select = cmdutil.OptionGroup(parser, "Selection options", "Control which revisions are listed. These options" " are mutually exclusive. If more than one is" " specified, the last is used.") - select.add_option("", "--archive", action="store_const", - const="archive", dest="type", default="archive", - help="List all revisions in the archive") - select.add_option("", "--cacherevs", action="store_const", - const="cacherevs", dest="type", - help="List all revisions stored in the archive as " - "complete copies") - select.add_option("", "--logs", action="store_const", - const="logs", dest="type", - help="List revisions that have a patchlog in the " - "tree") - select.add_option("", "--missing", action="store_const", - const="missing", dest="type", - help="List revisions from the specified version that" - " have no patchlog in the tree") - select.add_option("", "--skip-present", action="store_const", - const="skip-present", dest="type", - help="List revisions from the specified version that" - " have no patchlogs at all in the tree") - select.add_option("", "--present", action="store_const", - const="present", dest="type", - help="List revisions from the specified version that" - " have no patchlog in the tree, but can't be merged") - select.add_option("", "--missing-from", action="store_const", - const="missing-from", dest="type", - help="List revisions from the specified revision " - "that have no patchlog for the tree version") - select.add_option("", "--partner-missing", action="store_const", - const="partner-missing", dest="type", - help="List revisions in partner versions that are" - " missing") - select.add_option("", "--new-merges", action="store_const", - const="new-merges", dest="type", - help="List revisions that have had patchlogs added" - " to the tree since the last commit") - select.add_option("", "--direct-merges", action="store_const", - const="direct-merges", dest="type", - help="List revisions that have been directly added" - " to tree since the last commit ") - select.add_option("", "--library", action="store_const", - const="library", dest="type", - help="List revisions in the revision library") - select.add_option("", "--ancestry", action="store_const", - const="ancestry", dest="type", - help="List revisions that are ancestors of the " - "current tree version") - - select.add_option("", "--dependencies", action="store_const", - const="dependencies", dest="type", - help="List revisions that the given revision " - "depends on") - - select.add_option("", "--non-dependencies", action="store_const", - const="non-dependencies", dest="type", - help="List revisions that the given revision " - "does not depend on") - - select.add_option("--micro", action="store_const", - const="micro", dest="type", - help="List partner revisions aimed for this " - "micro-branch") - - select.add_option("", "--modified", dest="modified", - help="List tree ancestor revisions that modified a " - "given file", metavar="FILE[:LINE]") + cmdutil.add_revision_iter_options(select) parser.add_option("", "--skip", dest="skip", help="Skip revisions. Positive numbers skip from " "beginning, negative skip from end.", @@ -1312,6 +1174,9 @@ format.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") + format.add_option("--changelog", action="store_const", + const=self.changelog_append, dest="display", + help="Show location of cacherev file") parser.add_option_group(format) display = cmdutil.OptionGroup(parser, "Display format options", "These control the display of data") @@ -1448,6 +1313,7 @@ if os.access(self.history_file, os.R_OK) and \ os.path.isfile(self.history_file): readline.read_history_file(self.history_file) + self.cwd = os.getcwd() def write_history(self): readline.write_history_file(self.history_file) @@ -1470,16 +1336,21 @@ def set_prompt(self): if self.tree is not None: try: - version = " "+self.tree.tree_version.nonarch + prompt = pylon.alias_or_version(self.tree.tree_version, + self.tree, + full=False) + if prompt is not None: + prompt = " " + prompt except: - version = "" + prompt = "" else: - version = "" - self.prompt = "Fai%s> " % version + prompt = "" + self.prompt = "Fai%s> " % prompt def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1489,8 +1360,15 @@ def do_cd(self, line): if line == "": line = "~" + line = os.path.expanduser(line) + if os.path.isabs(line): + newcwd = line + else: + newcwd = self.cwd+'/'+line + newcwd = os.path.normpath(newcwd) try: - os.chdir(os.path.expanduser(line)) + os.chdir(newcwd) + self.cwd = newcwd except Exception, e: print e try: @@ -1523,7 +1401,7 @@ except cmdutil.CantDetermineRevision, e: print e except Exception, e: - print "Unhandled error:\n%s" % cmdutil.exception_str(e) + print "Unhandled error:\n%s" % errors.exception_str(e) elif suggestions.has_key(args[0]): print suggestions[args[0]] @@ -1574,7 +1452,7 @@ arg = line.split()[-1] else: arg = "" - iter = iter_munged_completions(iter, arg, text) + iter = cmdutil.iter_munged_completions(iter, arg, text) except Exception, e: print e return list(iter) @@ -1604,10 +1482,11 @@ else: arg = "" if arg.startswith("-"): - return list(iter_munged_completions(iter, arg, text)) + return list(cmdutil.iter_munged_completions(iter, arg, + text)) else: - return list(iter_munged_completions( - iter_file_completions(arg), arg, text)) + return list(cmdutil.iter_munged_completions( + cmdutil.iter_file_completions(arg), arg, text)) elif cmd == "cd": @@ -1615,13 +1494,13 @@ arg = args.split()[-1] else: arg = "" - iter = iter_dir_completions(arg) - iter = iter_munged_completions(iter, arg, text) + iter = cmdutil.iter_dir_completions(arg) + iter = cmdutil.iter_munged_completions(iter, arg, text) return list(iter) elif len(args)>0: arg = args.split()[-1] - return list(iter_munged_completions(iter_file_completions(arg), - arg, text)) + iter = cmdutil.iter_file_completions(arg) + return list(cmdutil.iter_munged_completions(iter, arg, text)) else: return self.completenames(text, line, begidx, endidx) except Exception, e: @@ -1636,44 +1515,8 @@ yield entry -def iter_file_completions(arg, only_dirs = False): - """Generate an iterator that iterates through filename completions. - - :param arg: The filename fragment to match - :type arg: str - :param only_dirs: If true, match only directories - :type only_dirs: bool - """ - cwd = os.getcwd() - if cwd != "/": - extras = [".", ".."] - else: - extras = [] - (dir, file) = os.path.split(arg) - if dir != "": - listingdir = os.path.expanduser(dir) - else: - listingdir = cwd - for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): - if dir != "": - userfile = dir+'/'+file - else: - userfile = file - if userfile.startswith(arg): - if os.path.isdir(listingdir+'/'+file): - userfile+='/' - yield userfile - elif not only_dirs: - yield userfile - -def iter_munged_completions(iter, arg, text): - for completion in iter: - completion = str(completion) - if completion.startswith(arg): - yield completion[len(arg)-len(text):] - def iter_source_file_completions(tree, arg): - treepath = cmdutil.tree_cwd(tree) + treepath = arch_compound.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: @@ -1701,7 +1544,7 @@ :return: An iterator of all matching untagged files :rtype: iterator of str """ - treepath = cmdutil.tree_cwd(tree) + treepath = arch_compound.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: @@ -1743,8 +1586,8 @@ :param arg: The prefix to match :type arg: str """ - treepath = cmdutil.tree_cwd(tree) - tmpdir = cmdutil.tmpdir() + treepath = arch_compound.tree_cwd(tree) + tmpdir = util.tmpdir() changeset = tmpdir+"/changeset" completions = [] revision = cmdutil.determine_revision_tree(tree) @@ -1756,14 +1599,6 @@ shutil.rmtree(tmpdir) return completions -def iter_dir_completions(arg): - """Generate an iterator that iterates through directory name completions. - - :param arg: The directory name fragment to match - :type arg: str - """ - return iter_file_completions(arg, True) - class Shell(BaseCommand): def __init__(self): self.description = "Runs Fai as a shell" @@ -1795,7 +1630,11 @@ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) - tree = arch.tree_root() + try: + tree = arch.tree_root() + except arch.errors.TreeRootError, e: + raise pylon.errors.CommandFailedWrapper(e) + if (len(args) == 0) == (options.untagged == False): raise cmdutil.GetHelp @@ -1809,13 +1648,22 @@ if options.id_type == "tagline": if method != "tagline": if not cmdutil.prompt("Tagline in other tree"): - if method == "explicit": - options.id_type == explicit + if method == "explicit" or method == "implicit": + options.id_type == method else: print "add-id not supported for \"%s\" tagging method"\ % method return + elif options.id_type == "implicit": + if method != "implicit": + if not cmdutil.prompt("Implicit in other tree"): + if method == "explicit" or method == "tagline": + options.id_type == method + else: + print "add-id not supported for \"%s\" tagging method"\ + % method + return elif options.id_type == "explicit": if method != "tagline" and method != explicit: if not prompt("Explicit in other tree"): @@ -1824,7 +1672,8 @@ return if options.id_type == "auto": - if method != "tagline" and method != "explicit": + if method != "tagline" and method != "explicit" \ + and method !="implicit": print "add-id not supported for \"%s\" tagging method" % method return else: @@ -1852,10 +1701,12 @@ previous_files.extend(files) if id_type == "explicit": cmdutil.add_id(files) - elif id_type == "tagline": + elif id_type == "tagline" or id_type == "implicit": for file in files: try: - cmdutil.add_tagline_or_explicit_id(file) + implicit = (id_type == "implicit") + cmdutil.add_tagline_or_explicit_id(file, False, + implicit) except cmdutil.AlreadyTagged: print "\"%s\" already has a tagline." % file except cmdutil.NoCommentSyntax: @@ -1888,6 +1739,9 @@ parser.add_option("--tagline", action="store_const", const="tagline", dest="id_type", help="Use a tagline id") + parser.add_option("--implicit", action="store_const", + const="implicit", dest="id_type", + help="Use an implicit id (deprecated)") parser.add_option("--untagged", action="store_true", dest="untagged", default=False, help="tag all untagged files") @@ -1926,27 +1780,7 @@ def get_completer(self, arg, index): if self.tree is None: raise arch.errors.TreeRootError - completions = list(ancillary.iter_partners(self.tree, - self.tree.tree_version)) - if len(completions) == 0: - completions = list(self.tree.iter_log_versions()) - - aliases = [] - try: - for completion in completions: - alias = ancillary.compact_alias(str(completion), self.tree) - if alias: - aliases.extend(alias) - - for completion in completions: - if completion.archive == self.tree.tree_version.archive: - aliases.append(completion.nonarch) - - except Exception, e: - print e - - completions.extend(aliases) - return completions + return cmdutil.merge_completions(self.tree, arg, index) def do_command(self, cmdargs): """ @@ -1961,7 +1795,7 @@ if self.tree is None: raise arch.errors.TreeRootError(os.getcwd()) - if cmdutil.has_changed(self.tree.tree_version): + if cmdutil.has_changed(ancillary.comp_revision(self.tree)): raise UncommittedChanges(self.tree) if len(args) > 0: @@ -2027,14 +1861,14 @@ :type other_revision: `arch.Revision` :return: 0 if the merge was skipped, 1 if it was applied """ - other_tree = cmdutil.find_or_make_local_revision(other_revision) + other_tree = arch_compound.find_or_make_local_revision(other_revision) try: if action == "native-merge": - ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, - other_revision) + ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, + other_revision) elif action == "update": - ancestor = cmdutil.tree_latest(self.tree, - other_revision.version) + ancestor = arch_compound.tree_latest(self.tree, + other_revision.version) except CantDetermineRevision, e: raise CommandFailedWrapper(e) cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) @@ -2104,7 +1938,10 @@ if self.tree is None: raise arch.errors.TreeRootError - edit_log(self.tree) + try: + edit_log(self.tree, self.tree.tree_version) + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) def get_parser(self): """ @@ -2132,7 +1969,7 @@ """ return -def edit_log(tree): +def edit_log(tree, version): """Makes and edits the log for a tree. Does all kinds of fancy things like log templates and merge summaries and log-for-merge @@ -2141,28 +1978,29 @@ """ #ensure we have an editor before preparing the log cmdutil.find_editor() - log = tree.log_message(create=False) + log = tree.log_message(create=False, version=version) log_is_new = False if log is None or cmdutil.prompt("Overwrite log"): if log is not None: os.remove(log.name) - log = tree.log_message(create=True) + log = tree.log_message(create=True, version=version) log_is_new = True tmplog = log.name - template = tree+"/{arch}/=log-template" - if not os.path.exists(template): - template = os.path.expanduser("~/.arch-params/=log-template") - if not os.path.exists(template): - template = None + template = pylon.log_template_path(tree) if template: shutil.copyfile(template, tmplog) - - new_merges = list(cmdutil.iter_new_merges(tree, - tree.tree_version)) - log["Summary"] = merge_summary(new_merges, tree.tree_version) + comp_version = ancillary.comp_revision(tree).version + new_merges = cmdutil.iter_new_merges(tree, comp_version) + new_merges = cmdutil.direct_merges(new_merges) + log["Summary"] = pylon.merge_summary(new_merges, + version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: @@ -2172,29 +2010,6 @@ os.remove(log.name) raise -def merge_summary(new_merges, tree_version): - if len(new_merges) == 0: - return "" - if len(new_merges) == 1: - summary = new_merges[0].summary - else: - summary = "Merge" - - credits = [] - for merge in new_merges: - if arch.my_id() != merge.creator: - name = re.sub("<.*>", "", merge.creator).rstrip(" "); - if not name in credits: - credits.append(name) - else: - version = merge.revision.version - if version.archive == tree_version.archive: - if not version.nonarch in credits: - credits.append(version.nonarch) - elif not str(version) in credits: - credits.append(str(version)) - - return ("%s (%s)") % (summary, ", ".join(credits)) class MirrorArchive(BaseCommand): """ @@ -2268,31 +2083,73 @@ Use "alias" to list available (user and automatic) aliases.""" +auto_alias = [ +"acur", +"The latest revision in the archive of the tree-version. You can specify \ +a different version like so: acur:foo--bar--0 (aliases can be used)", +"tcur", +"""(tree current) The latest revision in the tree of the tree-version. \ +You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ +used).""", +"tprev" , +"""(tree previous) The previous revision in the tree of the tree-version. To \ +specify an older revision, use a number, e.g. "tprev:4" """, +"tanc" , +"""(tree ancestor) The ancestor revision of the tree To specify an older \ +revision, use a number, e.g. "tanc:4".""", +"tdate" , +"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", +"tmod" , +""" (tree modified) The latest revision to modify a given file, e.g. \ +"tmod:engine.cpp" or "tmod:engine.cpp:16".""", +"ttag" , +"""(tree tag) The revision that was tagged into the current tree revision, \ +according to the tree""", +"tagcur", +"""(tag current) The latest revision of the version that the current tree \ +was tagged from.""", +"mergeanc" , +"""The common ancestor of the current tree and the specified revision. \ +Defaults to the first partner-version's latest revision or to tagcur.""", +] + + +def is_auto_alias(name): + """Determine whether a name is an auto alias name + + :param name: the name to check + :type name: str + :return: True if the name is an auto alias, false if not + :rtype: bool + """ + return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] + + +def display_def(iter, wrap = 80): + """Display a list of definitions + + :param iter: iter of name, definition pairs + :type iter: iter of (str, str) + :param wrap: The width for text wrapping + :type wrap: int + """ + vals = list(iter) + maxlen = 0 + for (key, value) in vals: + if len(key) > maxlen: + maxlen = len(key) + for (key, value) in vals: + tw=textwrap.TextWrapper(width=wrap, + initial_indent=key.rjust(maxlen)+" : ", + subsequent_indent="".rjust(maxlen+3)) + print tw.fill(value) + + def help_aliases(tree): - print """Auto-generated aliases - acur : The latest revision in the archive of the tree-version. You can specfy - a different version like so: acur:foo--bar--0 (aliases can be used) - tcur : (tree current) The latest revision in the tree of the tree-version. - You can specify a different version like so: tcur:foo--bar--0 (aliases - can be used). -tprev : (tree previous) The previous revision in the tree of the tree-version. - To specify an older revision, use a number, e.g. "tprev:4" - tanc : (tree ancestor) The ancestor revision of the tree - To specify an older revision, use a number, e.g. "tanc:4" -tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") - tmod : (tree modified) The latest revision to modify a given file - (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") - ttag : (tree tag) The revision that was tagged into the current tree revision, - according to the tree. -tagcur: (tag current) The latest revision of the version that the current tree - was tagged from. -mergeanc : The common ancestor of the current tree and the specified revision. - Defaults to the first partner-version's latest revision or to tagcur. - """ + print """Auto-generated aliases""" + display_def(pylon.util.iter_pairs(auto_alias)) print "User aliases" - for parts in ancillary.iter_all_alias(tree): - print parts[0].rjust(10)+" : "+parts[1] - + display_def(ancillary.iter_all_alias(tree)) class Inventory(BaseCommand): """List the status of files in the tree""" @@ -2428,6 +2285,11 @@ except cmdutil.ForbiddenAliasSyntax, e: raise CommandFailedWrapper(e) + def no_prefix(self, alias): + if alias.startswith("^"): + alias = alias[1:] + return alias + def arg_dispatch(self, args, options): """Add, modify, or list aliases, depending on number of arguments @@ -2438,15 +2300,20 @@ if len(args) == 0: help_aliases(self.tree) return - elif len(args) == 1: - self.print_alias(args[0]) - elif (len(args)) == 2: - self.add(args[0], args[1], options) else: - raise cmdutil.GetHelp + alias = self.no_prefix(args[0]) + if len(args) == 1: + self.print_alias(alias) + elif (len(args)) == 2: + self.add(alias, args[1], options) + else: + raise cmdutil.GetHelp def print_alias(self, alias): answer = None + if is_auto_alias(alias): + raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." + " Use \"revision\" to expand auto aliases." % alias) for pair in ancillary.iter_all_alias(self.tree): if pair[0] == alias: answer = pair[1] @@ -2464,6 +2331,8 @@ :type expansion: str :param options: The commandline options """ + if is_auto_alias(alias): + raise IsAutoAlias(alias) newlist = "" written = False new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, @@ -2490,14 +2359,17 @@ deleted = False if len(args) != 1: raise cmdutil.GetHelp + alias = self.no_prefix(args[0]) + if is_auto_alias(alias): + raise IsAutoAlias(alias) newlist = "" for pair in self.get_iterator(options): - if pair[0] != args[0]: + if pair[0] != alias: newlist+="%s=%s\n" % (pair[0], pair[1]) else: deleted = True if not deleted: - raise errors.NoSuchAlias(args[0]) + raise errors.NoSuchAlias(alias) self.write_aliases(newlist, options) def get_alias_file(self, options): @@ -2526,7 +2398,7 @@ :param options: The commandline options """ filename = os.path.expanduser(self.get_alias_file(options)) - file = cmdutil.NewFileVersion(filename) + file = util.NewFileVersion(filename) file.write(newlist) file.commit() @@ -2588,10 +2460,13 @@ :param cmdargs: The commandline arguments :type cmdargs: list of str """ - cmdutil.find_editor() parser = self.get_parser() (options, args) = parser.parse_args(cmdargs) try: + cmdutil.find_editor() + except pylon.errors.NoEditorSpecified, e: + raise pylon.errors.CommandFailedWrapper(e) + try: self.tree=arch.tree_root() except: self.tree=None @@ -2655,7 +2530,7 @@ target_revision = cmdutil.determine_revision_arch(self.tree, args[0]) else: - target_revision = cmdutil.tree_latest(self.tree) + target_revision = arch_compound.tree_latest(self.tree) if len(args) > 1: merges = [ arch.Patchlog(cmdutil.determine_revision_arch( self.tree, f)) for f in args[1:] ] @@ -2711,7 +2586,7 @@ :param message: The message to send :type message: `email.Message`""" - server = smtplib.SMTP() + server = smtplib.SMTP("localhost") server.sendmail(message['From'], message['To'], message.as_string()) server.quit() @@ -2763,6 +2638,22 @@ 'alias' : Alias, 'request-merge': RequestMerge, } + +def my_import(mod_name): + module = __import__(mod_name) + components = mod_name.split('.') + for comp in components[1:]: + module = getattr(module, comp) + return module + +def plugin(mod_name): + module = my_import(mod_name) + module.add_command(commands) + +for file in os.listdir(sys.path[0]+"/command"): + if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": + plugin("command."+file[:-3]) + suggestions = { 'apply-delta' : "Try \"apply-changes\".", 'delta' : "To compare two revisions, use \"changes\".", @@ -2784,6 +2675,7 @@ 'tagline' : "Use add-id. It uses taglines in tagline trees", 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", 'library-revisions' : "Use revisions --library", -'file-revert' : "Use revert FILE" +'file-revert' : "Use revert FILE", +'join-branch' : "Use replay --logs-only" } # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 patchkit-0.1.8/test_patches_data/diff-2000064400000000000000000000004161046102023000161130ustar 00000000000000--- patches.py +++ patches.py @@ -391,6 +391,8 @@ else: assert isinstance(hunk_line, RemoveLine) line_no += 1 + for line in orig_lines: + yield line import unittest import os.path patchkit-0.1.8/test_patches_data/diff-3000064400000000000000000000003141046102023000161110ustar 00000000000000--- orig-3 2005-09-23 16:23:20.000000000 -0500 +++ mod-3 2005-09-23 16:23:38.000000000 -0500 @@ -1,3 +1,4 @@ +First line change # Copyright (C) 2004, 2005 Aaron Bentley # # patchkit-0.1.8/test_patches_data/diff-4000064400000000000000000000003371046102023000161170ustar 00000000000000--- orig-4 2005-09-23 16:24:21.000000000 -0500 +++ mod-4 2005-09-23 16:24:35.000000000 -0500 @@ -555,4 +555,4 @@ if __name__ == "__main__": test() -# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 +last line change patchkit-0.1.8/test_patches_data/diff-5000064400000000000000000000116401046102023000161170ustar 00000000000000--- orig-5 2005-09-23 16:25:00.000000000 -0500 +++ mod-5 2005-09-23 16:25:21.000000000 -0500 @@ -60,161 +60,6 @@ raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) -def parse_range(textrange): - """Parse a patch range, handling the "1" special-case - - :param textrange: The text to parse - :type textrange: str - :return: the position and range, as a tuple - :rtype: (int, int) - """ - tmp = textrange.split(',') - if len(tmp) == 1: - pos = tmp[0] - range = "1" - else: - (pos, range) = tmp - pos = int(pos) - range = int(range) - return (pos, range) - - -def hunk_from_header(line): - if not line.startswith("@@") or not line.endswith("@@\n") \ - or not len(line) > 4: - raise MalformedHunkHeader("Does not start and end with @@.", line) - try: - (orig, mod) = line[3:-4].split(" ") - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if not orig.startswith('-') or not mod.startswith('+'): - raise MalformedHunkHeader("Positions don't start with + or -.", line) - try: - (orig_pos, orig_range) = parse_range(orig[1:]) - (mod_pos, mod_range) = parse_range(mod[1:]) - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if mod_range < 0 or orig_range < 0: - raise MalformedHunkHeader("Hunk range is negative", line) - return Hunk(orig_pos, orig_range, mod_pos, mod_range) - - -class HunkLine: - def __init__(self, contents): - self.contents = contents - - def get_str(self, leadchar): - if self.contents == "\n" and leadchar == " " and False: - return "\n" - if not self.contents.endswith('\n'): - terminator = '\n' + NO_NL - else: - terminator = '' - return leadchar + self.contents + terminator - - -class ContextLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str(" ") - - -class InsertLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("+") - - -class RemoveLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("-") - -NO_NL = '\\ No newline at end of file\n' -__pychecker__="no-returnvalues" - -def parse_line(line): - if line.startswith("\n"): - return ContextLine(line) - elif line.startswith(" "): - return ContextLine(line[1:]) - elif line.startswith("+"): - return InsertLine(line[1:]) - elif line.startswith("-"): - return RemoveLine(line[1:]) - elif line == NO_NL: - return NO_NL - else: - raise MalformedLine("Unknown line type", line) -__pychecker__="" - - -class Hunk: - def __init__(self, orig_pos, orig_range, mod_pos, mod_range): - self.orig_pos = orig_pos - self.orig_range = orig_range - self.mod_pos = mod_pos - self.mod_range = mod_range - self.lines = [] - - def get_header(self): - return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, - self.orig_range), - self.range_str(self.mod_pos, - self.mod_range)) - - def range_str(self, pos, range): - """Return a file range, special-casing for 1-line files. - - :param pos: The position in the file - :type pos: int - :range: The range in the file - :type range: int - :return: a string in the format 1,4 except when range == pos == 1 - """ - if range == 1: - return "%i" % pos - else: - return "%i,%i" % (pos, range) - - def __str__(self): - lines = [self.get_header()] - for line in self.lines: - lines.append(str(line)) - return "".join(lines) - - def shift_to_mod(self, pos): - if pos < self.orig_pos-1: - return 0 - elif pos > self.orig_pos+self.orig_range: - return self.mod_range - self.orig_range - else: - return self.shift_to_mod_lines(pos) - - def shift_to_mod_lines(self, pos): - assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) - position = self.orig_pos-1 - shift = 0 - for line in self.lines: - if isinstance(line, InsertLine): - shift += 1 - elif isinstance(line, RemoveLine): - if position == pos: - return None - shift -= 1 - position += 1 - elif isinstance(line, ContextLine): - position += 1 - if position > pos: - break - return shift - def iter_hunks(iter_lines): hunk = None for line in iter_lines: patchkit-0.1.8/test_patches_data/diff-6000064400000000000000000000453641046102023000161320ustar 00000000000000--- orig-6 2005-09-23 16:27:16.000000000 -0500 +++ mod-6 2005-09-23 16:27:32.000000000 -0500 @@ -1,558 +1 @@ -# Copyright (C) 2004, 2005 Aaron Bentley -# -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -class PatchSyntax(Exception): - def __init__(self, msg): - Exception.__init__(self, msg) - - -class MalformedPatchHeader(PatchSyntax): - def __init__(self, desc, line): - self.desc = desc - self.line = line - msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) - PatchSyntax.__init__(self, msg) - -class MalformedHunkHeader(PatchSyntax): - def __init__(self, desc, line): - self.desc = desc - self.line = line - msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) - PatchSyntax.__init__(self, msg) - -class MalformedLine(PatchSyntax): - def __init__(self, desc, line): - self.desc = desc - self.line = line - msg = "Malformed line. %s\n%s" % (self.desc, self.line) - PatchSyntax.__init__(self, msg) - -def get_patch_names(iter_lines): - try: - line = iter_lines.next() - if not line.startswith("--- "): - raise MalformedPatchHeader("No orig name", line) - else: - orig_name = line[4:].rstrip("\n") - except StopIteration: - raise MalformedPatchHeader("No orig line", "") - try: - line = iter_lines.next() - if not line.startswith("+++ "): - raise PatchSyntax("No mod name") - else: - mod_name = line[4:].rstrip("\n") - except StopIteration: - raise MalformedPatchHeader("No mod line", "") - return (orig_name, mod_name) - -def parse_range(textrange): - """Parse a patch range, handling the "1" special-case - - :param textrange: The text to parse - :type textrange: str - :return: the position and range, as a tuple - :rtype: (int, int) - """ - tmp = textrange.split(',') - if len(tmp) == 1: - pos = tmp[0] - range = "1" - else: - (pos, range) = tmp - pos = int(pos) - range = int(range) - return (pos, range) - - -def hunk_from_header(line): - if not line.startswith("@@") or not line.endswith("@@\n") \ - or not len(line) > 4: - raise MalformedHunkHeader("Does not start and end with @@.", line) - try: - (orig, mod) = line[3:-4].split(" ") - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if not orig.startswith('-') or not mod.startswith('+'): - raise MalformedHunkHeader("Positions don't start with + or -.", line) - try: - (orig_pos, orig_range) = parse_range(orig[1:]) - (mod_pos, mod_range) = parse_range(mod[1:]) - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if mod_range < 0 or orig_range < 0: - raise MalformedHunkHeader("Hunk range is negative", line) - return Hunk(orig_pos, orig_range, mod_pos, mod_range) - - -class HunkLine: - def __init__(self, contents): - self.contents = contents - - def get_str(self, leadchar): - if self.contents == "\n" and leadchar == " " and False: - return "\n" - if not self.contents.endswith('\n'): - terminator = '\n' + NO_NL - else: - terminator = '' - return leadchar + self.contents + terminator - - -class ContextLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str(" ") - - -class InsertLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("+") - - -class RemoveLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("-") - -NO_NL = '\\ No newline at end of file\n' -__pychecker__="no-returnvalues" - -def parse_line(line): - if line.startswith("\n"): - return ContextLine(line) - elif line.startswith(" "): - return ContextLine(line[1:]) - elif line.startswith("+"): - return InsertLine(line[1:]) - elif line.startswith("-"): - return RemoveLine(line[1:]) - elif line == NO_NL: - return NO_NL - else: - raise MalformedLine("Unknown line type", line) -__pychecker__="" - - -class Hunk: - def __init__(self, orig_pos, orig_range, mod_pos, mod_range): - self.orig_pos = orig_pos - self.orig_range = orig_range - self.mod_pos = mod_pos - self.mod_range = mod_range - self.lines = [] - - def get_header(self): - return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, - self.orig_range), - self.range_str(self.mod_pos, - self.mod_range)) - - def range_str(self, pos, range): - """Return a file range, special-casing for 1-line files. - - :param pos: The position in the file - :type pos: int - :range: The range in the file - :type range: int - :return: a string in the format 1,4 except when range == pos == 1 - """ - if range == 1: - return "%i" % pos - else: - return "%i,%i" % (pos, range) - - def __str__(self): - lines = [self.get_header()] - for line in self.lines: - lines.append(str(line)) - return "".join(lines) - - def shift_to_mod(self, pos): - if pos < self.orig_pos-1: - return 0 - elif pos > self.orig_pos+self.orig_range: - return self.mod_range - self.orig_range - else: - return self.shift_to_mod_lines(pos) - - def shift_to_mod_lines(self, pos): - assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) - position = self.orig_pos-1 - shift = 0 - for line in self.lines: - if isinstance(line, InsertLine): - shift += 1 - elif isinstance(line, RemoveLine): - if position == pos: - return None - shift -= 1 - position += 1 - elif isinstance(line, ContextLine): - position += 1 - if position > pos: - break - return shift - -def iter_hunks(iter_lines): - hunk = None - for line in iter_lines: - if line == "\n": - if hunk is not None: - yield hunk - hunk = None - continue - if hunk is not None: - yield hunk - hunk = hunk_from_header(line) - orig_size = 0 - mod_size = 0 - while orig_size < hunk.orig_range or mod_size < hunk.mod_range: - hunk_line = parse_line(iter_lines.next()) - hunk.lines.append(hunk_line) - if isinstance(hunk_line, (RemoveLine, ContextLine)): - orig_size += 1 - if isinstance(hunk_line, (InsertLine, ContextLine)): - mod_size += 1 - if hunk is not None: - yield hunk - -class Patch: - def __init__(self, oldname, newname): - self.oldname = oldname - self.newname = newname - self.hunks = [] - - def __str__(self): - ret = self.get_header() - ret += "".join([str(h) for h in self.hunks]) - return ret - - def get_header(self): - return "--- %s\n+++ %s\n" % (self.oldname, self.newname) - - def stats_str(self): - """Return a string of patch statistics""" - removes = 0 - inserts = 0 - for hunk in self.hunks: - for line in hunk.lines: - if isinstance(line, InsertLine): - inserts+=1; - elif isinstance(line, RemoveLine): - removes+=1; - return "%i inserts, %i removes in %i hunks" % \ - (inserts, removes, len(self.hunks)) - - def pos_in_mod(self, position): - newpos = position - for hunk in self.hunks: - shift = hunk.shift_to_mod(position) - if shift is None: - return None - newpos += shift - return newpos - - def iter_inserted(self): - """Iteraties through inserted lines - - :return: Pair of line number, line - :rtype: iterator of (int, InsertLine) - """ - for hunk in self.hunks: - pos = hunk.mod_pos - 1; - for line in hunk.lines: - if isinstance(line, InsertLine): - yield (pos, line) - pos += 1 - if isinstance(line, ContextLine): - pos += 1 - -def parse_patch(iter_lines): - (orig_name, mod_name) = get_patch_names(iter_lines) - patch = Patch(orig_name, mod_name) - for hunk in iter_hunks(iter_lines): - patch.hunks.append(hunk) - return patch - - -def iter_file_patch(iter_lines): - saved_lines = [] - for line in iter_lines: - if line.startswith('=== '): - continue - elif line.startswith('--- '): - if len(saved_lines) > 0: - yield saved_lines - saved_lines = [] - saved_lines.append(line) - if len(saved_lines) > 0: - yield saved_lines - - -def iter_lines_handle_nl(iter_lines): - """ - Iterates through lines, ensuring that lines that originally had no - terminating \n are produced without one. This transformation may be - applied at any point up until hunk line parsing, and is safe to apply - repeatedly. - """ - last_line = None - for line in iter_lines: - if line == NO_NL: - assert last_line.endswith('\n') - last_line = last_line[:-1] - line = None - if last_line is not None: - yield last_line - last_line = line - if last_line is not None: - yield last_line - - -def parse_patches(iter_lines): - iter_lines = iter_lines_handle_nl(iter_lines) - return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] - - -def difference_index(atext, btext): - """Find the indext of the first character that differs betweeen two texts - - :param atext: The first text - :type atext: str - :param btext: The second text - :type str: str - :return: The index, or None if there are no differences within the range - :rtype: int or NoneType - """ - length = len(atext) - if len(btext) < length: - length = len(btext) - for i in range(length): - if atext[i] != btext[i]: - return i; - return None - -class PatchConflict(Exception): - def __init__(self, line_no, orig_line, patch_line): - orig = orig_line.rstrip('\n') - patch = str(patch_line).rstrip('\n') - msg = 'Text contents mismatch at line %d. Original has "%s",'\ - ' but patch says it should be "%s"' % (line_no, orig, patch) - Exception.__init__(self, msg) - - -def iter_patched(orig_lines, patch_lines): - """Iterate through a series of lines with a patch applied. - This handles a single file, and does exact, not fuzzy patching. - """ - if orig_lines is not None: - orig_lines = orig_lines.__iter__() - seen_patch = [] - patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) - get_patch_names(patch_lines) - line_no = 1 - for hunk in iter_hunks(patch_lines): - while line_no < hunk.orig_pos: - orig_line = orig_lines.next() - yield orig_line - line_no += 1 - for hunk_line in hunk.lines: - seen_patch.append(str(hunk_line)) - if isinstance(hunk_line, InsertLine): - yield hunk_line.contents - elif isinstance(hunk_line, (ContextLine, RemoveLine)): - orig_line = orig_lines.next() - if orig_line != hunk_line.contents: - raise PatchConflict(line_no, orig_line, "".join(seen_patch)) - if isinstance(hunk_line, ContextLine): - yield orig_line - else: - assert isinstance(hunk_line, RemoveLine) - line_no += 1 - -import unittest -import os.path -class PatchesTester(unittest.TestCase): - def datafile(self, filename): - data_path = os.path.join(os.path.dirname(__file__), "testdata", - filename) - return file(data_path, "rb") - - def testValidPatchHeader(self): - """Parse a valid patch header""" - lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') - (orig, mod) = get_patch_names(lines.__iter__()) - assert(orig == "orig/commands.py") - assert(mod == "mod/dommands.py") - - def testInvalidPatchHeader(self): - """Parse an invalid patch header""" - lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') - self.assertRaises(MalformedPatchHeader, get_patch_names, - lines.__iter__()) - - def testValidHunkHeader(self): - """Parse a valid hunk header""" - header = "@@ -34,11 +50,6 @@\n" - hunk = hunk_from_header(header); - assert (hunk.orig_pos == 34) - assert (hunk.orig_range == 11) - assert (hunk.mod_pos == 50) - assert (hunk.mod_range == 6) - assert (str(hunk) == header) - - def testValidHunkHeader2(self): - """Parse a tricky, valid hunk header""" - header = "@@ -1 +0,0 @@\n" - hunk = hunk_from_header(header); - assert (hunk.orig_pos == 1) - assert (hunk.orig_range == 1) - assert (hunk.mod_pos == 0) - assert (hunk.mod_range == 0) - assert (str(hunk) == header) - - def makeMalformed(self, header): - self.assertRaises(MalformedHunkHeader, hunk_from_header, header) - - def testInvalidHeader(self): - """Parse an invalid hunk header""" - self.makeMalformed(" -34,11 +50,6 \n") - self.makeMalformed("@@ +50,6 -34,11 @@\n") - self.makeMalformed("@@ -34,11 +50,6 @@") - self.makeMalformed("@@ -34.5,11 +50,6 @@\n") - self.makeMalformed("@@-34,11 +50,6@@\n") - self.makeMalformed("@@ 34,11 50,6 @@\n") - self.makeMalformed("@@ -34,11 @@\n") - self.makeMalformed("@@ -34,11 +50,6.5 @@\n") - self.makeMalformed("@@ -34,11 +50,-6 @@\n") - - def lineThing(self,text, type): - line = parse_line(text) - assert(isinstance(line, type)) - assert(str(line)==text) - - def makeMalformedLine(self, text): - self.assertRaises(MalformedLine, parse_line, text) - - def testValidLine(self): - """Parse a valid hunk line""" - self.lineThing(" hello\n", ContextLine) - self.lineThing("+hello\n", InsertLine) - self.lineThing("-hello\n", RemoveLine) - - def testMalformedLine(self): - """Parse invalid valid hunk lines""" - self.makeMalformedLine("hello\n") - - def compare_parsed(self, patchtext): - lines = patchtext.splitlines(True) - patch = parse_patch(lines.__iter__()) - pstr = str(patch) - i = difference_index(patchtext, pstr) - if i is not None: - print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) - self.assertEqual (patchtext, str(patch)) - - def testAll(self): - """Test parsing a whole patch""" - patchtext = """--- orig/commands.py -+++ mod/commands.py -@@ -1337,7 +1337,8 @@ - - def set_title(self, command=None): - try: -- version = self.tree.tree_version.nonarch -+ version = pylon.alias_or_version(self.tree.tree_version, self.tree, -+ full=False) - except: - version = "[no version]" - if command is None: -@@ -1983,7 +1984,11 @@ - version) - if len(new_merges) > 0: - if cmdutil.prompt("Log for merge"): -- mergestuff = cmdutil.log_for_merge(tree, comp_version) -+ if cmdutil.prompt("changelog for merge"): -+ mergestuff = "Patches applied:\\n" -+ mergestuff += pylon.changelog_for_merge(new_merges) -+ else: -+ mergestuff = cmdutil.log_for_merge(tree, comp_version) - log.description += mergestuff - log.save() - try: -""" - self.compare_parsed(patchtext) - - def testInit(self): - """Handle patches missing half the position, range tuple""" - patchtext = \ -"""--- orig/__init__.py -+++ mod/__init__.py -@@ -1 +1,2 @@ - __docformat__ = "restructuredtext en" -+__doc__ = An alternate Arch commandline interface -""" - self.compare_parsed(patchtext) - - - - def testLineLookup(self): - import sys - """Make sure we can accurately look up mod line from orig""" - patch = parse_patch(self.datafile("diff")) - orig = list(self.datafile("orig")) - mod = list(self.datafile("mod")) - removals = [] - for i in range(len(orig)): - mod_pos = patch.pos_in_mod(i) - if mod_pos is None: - removals.append(orig[i]) - continue - assert(mod[mod_pos]==orig[i]) - rem_iter = removals.__iter__() - for hunk in patch.hunks: - for line in hunk.lines: - if isinstance(line, RemoveLine): - next = rem_iter.next() - if line.contents != next: - sys.stdout.write(" orig:%spatch:%s" % (next, - line.contents)) - assert(line.contents == next) - self.assertRaises(StopIteration, rem_iter.next) - - def testFirstLineRenumber(self): - """Make sure we handle lines at the beginning of the hunk""" - patch = parse_patch(self.datafile("insert_top.patch")) - assert (patch.pos_in_mod(0)==1) - -def test(): - patchesTestSuite = unittest.makeSuite(PatchesTester,'test') - runner = unittest.TextTestRunner(verbosity=0) - return runner.run(patchesTestSuite) - - -if __name__ == "__main__": - test() -# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 +Total contents change patchkit-0.1.8/test_patches_data/diff-7000064400000000000000000000003141046102023000161150ustar 00000000000000--- orig-7 2008-12-02 01:58:43.000000000 +0100 +++ mod-7 2008-12-02 01:58:43.000000000 +0100 @@ -1 +1 @@ -No terminating newline \ No newline at end of file +No newline either \ No newline at end of file patchkit-0.1.8/test_patches_data/insert_top.patch000064400000000000000000000002021046102023000203210ustar 00000000000000--- orig/pylon/patches.py +++ mod/pylon/patches.py @@ -1,3 +1,4 @@ +#test import util import sys class PatchSyntax(Exception): patchkit-0.1.8/test_patches_data/mod000064400000000000000000002677631046102023000156470ustar 00000000000000# Copyright (C) 2004 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import sys import arch import arch.util import arch.arch import pylon.errors from pylon.errors import * from pylon import errors from pylon import util from pylon import arch_core from pylon import arch_compound from pylon import ancillary from pylon import misc from pylon import paths import abacmds import cmdutil import shutil import os import options import time import cmd import readline import re import string import terminal import email import smtplib import textwrap __docformat__ = "restructuredtext" __doc__ = "Implementation of user (sub) commands" commands = {} def find_command(cmd): """ Return an instance of a command type. Return None if the type isn't registered. :param cmd: the name of the command to look for :type cmd: the type of the command """ if commands.has_key(cmd): return commands[cmd]() else: return None class BaseCommand: def __call__(self, cmdline): try: self.do_command(cmdline.split()) except cmdutil.GetHelp, e: self.help() except Exception, e: print e def get_completer(index): return None def complete(self, args, text): """ Returns a list of possible completions for the given text. :param args: The complete list of arguments :type args: List of str :param text: text to complete (may be shorter than args[-1]) :type text: str :rtype: list of str """ matches = [] candidates = None if len(args) > 0: realtext = args[-1] else: realtext = "" try: parser=self.get_parser() if realtext.startswith('-'): candidates = parser.iter_options() else: (options, parsed_args) = parser.parse_args(args) if len (parsed_args) > 0: candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) else: candidates = self.get_completer("", 0) except: pass if candidates is None: return for candidate in candidates: candidate = str(candidate) if candidate.startswith(realtext): matches.append(candidate[len(realtext)- len(text):]) return matches class Help(BaseCommand): """ Lists commands, prints help messages. """ def __init__(self): self.description="Prints help mesages" self.parser = None def do_command(self, cmdargs): """ Prints a help message. """ options, args = self.get_parser().parse_args(cmdargs) if len(args) > 1: raise cmdutil.GetHelp if options.native or options.suggestions or options.external: native = options.native suggestions = options.suggestions external = options.external else: native = True suggestions = False external = True if len(args) == 0: self.list_commands(native, suggestions, external) return elif len(args) == 1: command_help(args[0]) return def help(self): self.get_parser().print_help() print """ If no command is specified, commands are listed. If a command is specified, help for that command is listed. """ def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ if self.parser is not None: return self.parser parser=cmdutil.CmdOptionParser("fai help [command]") parser.add_option("-n", "--native", action="store_true", dest="native", help="Show native commands") parser.add_option("-e", "--external", action="store_true", dest="external", help="Show external commands") parser.add_option("-s", "--suggest", action="store_true", dest="suggestions", help="Show suggestions") self.parser = parser return parser def list_commands(self, native=True, suggest=False, external=True): """ Lists supported commands. :param native: list native, python-based commands :type native: bool :param external: list external aba-style commands :type external: bool """ if native: print "Native Fai commands" keys=commands.keys() keys.sort() for k in keys: space="" for i in range(28-len(k)): space+=" " print space+k+" : "+commands[k]().description print if suggest: print "Unavailable commands and suggested alternatives" key_list = suggestions.keys() key_list.sort() for key in key_list: print "%28s : %s" % (key, suggestions[key]) print if external: fake_aba = abacmds.AbaCmds() if (fake_aba.abadir == ""): return print "External commands" fake_aba.list_commands() print if not suggest: print "Use help --suggest to list alternatives to tla and aba"\ " commands." if options.tla_fallthrough and (native or external): print "Fai also supports tla commands." def command_help(cmd): """ Prints help for a command. :param cmd: The name of the command to print help for :type cmd: str """ fake_aba = abacmds.AbaCmds() cmdobj = find_command(cmd) if cmdobj != None: cmdobj.help() elif suggestions.has_key(cmd): print "Not available\n" + suggestions[cmd] else: abacmd = fake_aba.is_command(cmd) if abacmd: abacmd.help() else: print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" class Changes(BaseCommand): """ the "changes" command: lists differences between trees/revisions: """ def __init__(self): self.description="Lists what files have changed in the project tree" def get_completer(self, arg, index): if index > 1: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) > 2: raise cmdutil.GetHelp tree=arch.tree_root() if len(args) == 0: a_spec = ancillary.comp_revision(tree) else: a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) if len(args) == 2: b_spec = cmdutil.determine_revision_tree(tree, args[1]) cmdutil.ensure_archive_registered(b_spec.archive) else: b_spec=tree return options, a_spec, b_spec def do_command(self, cmdargs): """ Master function that perfoms the "changes" command. """ try: options, a_spec, b_spec = self.parse_commandline(cmdargs); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return if options.changeset: changeset=options.changeset tmpdir = None else: tmpdir=util.tmpdir() changeset=tmpdir+"/changeset" try: delta=arch.iter_delta(a_spec, b_spec, changeset) try: for line in delta: if cmdutil.chattermatch(line, "changeset:"): pass else: cmdutil.colorize(line, options.suppress_chatter) except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "missing explicit id for file"): raise MissingID(e) else: raise status=delta.status if status > 1: return if (options.perform_diff): chan = arch_compound.ChangesetMunger(changeset) chan.read_indices() if options.diffopts is not None: if isinstance(b_spec, arch.Revision): b_dir = b_spec.library_find() else: b_dir = b_spec a_dir = a_spec.library_find() diffopts = options.diffopts.split() cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) else: cmdutil.show_diffs(delta.changeset) finally: if tmpdir and (os.access(tmpdir, os.X_OK)): shutil.rmtree(tmpdir) def get_parser(self): """ Returns the options parser to use for the "changes" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" " [revision]") parser.add_option("-d", "--diff", action="store_true", dest="perform_diff", default=False, help="Show diffs in summary") parser.add_option("-c", "--changeset", dest="changeset", help="Store a changeset in the given directory", metavar="DIRECTORY") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") parser.add_option("--diffopts", dest="diffopts", help="Use the specified diff options", metavar="OPTIONS") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Performs source-tree comparisons If no revision is specified, the current project tree is compared to the last-committed revision. If one revision is specified, the current project tree is compared to that revision. If two revisions are specified, they are compared to each other. """ help_tree_spec() return class ApplyChanges(BaseCommand): """ Apply differences between two revisions to a tree """ def __init__(self): self.description="Applies changes to a project tree" def get_completer(self, arg, index): if index > 1: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) != 2: raise cmdutil.GetHelp a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) b_spec = cmdutil.determine_revision_tree(tree, args[1]) cmdutil.ensure_archive_registered(b_spec.archive) return options, a_spec, b_spec def do_command(self, cmdargs): """ Master function that performs "apply-changes". """ try: tree = arch.tree_root() options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return delta=cmdutil.apply_delta(a_spec, b_spec, tree) for line in cmdutil.iter_apply_delta_filter(delta): cmdutil.colorize(line, options.suppress_chatter) def get_parser(self): """ Returns the options parser to use for the "apply-changes" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" " revision") parser.add_option("-d", "--diff", action="store_true", dest="perform_diff", default=False, help="Show diffs in summary") parser.add_option("-c", "--changeset", dest="changeset", help="Store a changeset in the given directory", metavar="DIRECTORY") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Applies changes to a project tree Compares two revisions and applies the difference between them to the current tree. """ help_tree_spec() return class Update(BaseCommand): """ Updates a project tree to a given revision, preserving un-committed hanges. """ def __init__(self): self.description="Apply the latest changes to the current directory" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) > 2: raise cmdutil.GetHelp spec=None if len(args)>0: spec=args[0] revision=cmdutil.determine_revision_arch(tree, spec) cmdutil.ensure_archive_registered(revision.archive) mirror_source = cmdutil.get_mirror_source(revision.archive) if mirror_source != None: if cmdutil.prompt("Mirror update"): cmd=cmdutil.mirror_archive(mirror_source, revision.archive, arch.NameParser(revision).get_package_version()) for line in arch.chatter_classifier(cmd): cmdutil.colorize(line, options.suppress_chatter) revision=cmdutil.determine_revision_arch(tree, spec) return options, revision def do_command(self, cmdargs): """ Master function that perfoms the "update" command. """ tree=arch.tree_root() try: options, to_revision = self.parse_commandline(cmdargs, tree); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return from_revision = arch_compound.tree_latest(tree) if from_revision==to_revision: print "Tree is already up to date with:\n"+str(to_revision)+"." return cmdutil.ensure_archive_registered(from_revision.archive) cmd=cmdutil.apply_delta(from_revision, to_revision, tree, options.patch_forward) for line in cmdutil.iter_apply_delta_filter(cmd): cmdutil.colorize(line) if to_revision.version != tree.tree_version: if cmdutil.prompt("Update version"): tree.tree_version = to_revision.version def get_parser(self): """ Returns the options parser to use for the "update" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai update [options]" " [revision/version]") parser.add_option("-f", "--forward", action="store_true", dest="patch_forward", default=False, help="pass the --forward option to 'patch'") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Updates a working tree to the current archive revision If a revision or version is specified, that is used instead """ help_tree_spec() return class Commit(BaseCommand): """ Create a revision based on the changes in the current tree. """ def __init__(self): self.description="Write local changes to the archive" def get_completer(self, arg, index): if arg is None: arg = "" return iter_modified_file_completions(arch.tree_root(), arg) # return iter_source_file_completions(arch.tree_root(), arg) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) == 0: args = None if options.version is None: return options, tree.tree_version, args revision=cmdutil.determine_revision_arch(tree, options.version) return options, revision.get_version(), args def do_command(self, cmdargs): """ Master function that perfoms the "commit" command. """ tree=arch.tree_root() options, version, files = self.parse_commandline(cmdargs, tree) ancestor = None if options.__dict__.has_key("base") and options.base: base = cmdutil.determine_revision_tree(tree, options.base) ancestor = base else: base = ancillary.submit_revision(tree) ancestor = base if ancestor is None: ancestor = arch_compound.tree_latest(tree, version) writeversion=version archive=version.archive source=cmdutil.get_mirror_source(archive) allow_old=False writethrough="implicit" if source!=None: if writethrough=="explicit" and \ cmdutil.prompt("Writethrough"): writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) elif writethrough=="none": raise CommitToMirror(archive) elif archive.is_mirror: raise CommitToMirror(archive) try: last_revision=tree.iter_logs(version, True).next().revision except StopIteration, e: last_revision = None if ancestor is None: if cmdutil.prompt("Import from commit"): return do_import(version) else: raise NoVersionLogs(version) try: arch_last_revision = version.iter_revisions(True).next() except StopIteration, e: arch_last_revision = None if last_revision != arch_last_revision: print "Tree is not up to date with %s" % str(version) if not cmdutil.prompt("Out of date"): raise OutOfDate else: allow_old=True try: if not cmdutil.has_changed(ancestor): if not cmdutil.prompt("Empty commit"): raise EmptyCommit except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "missing explicit id for file"): raise MissingID(e) else: raise log = tree.log_message(create=False, version=version) if log is None: try: if cmdutil.prompt("Create log"): edit_log(tree, version) except cmdutil.NoEditorSpecified, e: raise CommandFailed(e) log = tree.log_message(create=False, version=version) if log is None: raise NoLogMessage if log["Summary"] is None or len(log["Summary"].strip()) == 0: if not cmdutil.prompt("Omit log summary"): raise errors.NoLogSummary try: for line in tree.iter_commit(version, seal=options.seal_version, base=base, out_of_date_ok=allow_old, file_list=files): cmdutil.colorize(line, options.suppress_chatter) except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "These files violate naming conventions:"): raise LintFailure(e.proc.error) else: raise def get_parser(self): """ Returns the options parser to use for the "commit" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" " [file2...]") parser.add_option("--seal", action="store_true", dest="seal_version", default=False, help="seal this version") parser.add_option("-v", "--version", dest="version", help="Use the specified version", metavar="VERSION") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") if cmdutil.supports_switch("commit", "--base"): parser.add_option("--base", dest="base", help="", metavar="REVISION") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Updates a working tree to the current archive revision If a version is specified, that is used instead """ # help_tree_spec() return class CatLog(BaseCommand): """ Print the log of a given file (from current tree) """ def __init__(self): self.description="Prints the patch log for a revision" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "cat-log" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: tree = None spec=None if len(args) > 0: spec=args[0] if len(args) > 1: raise cmdutil.GetHelp() try: if tree: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.determine_revision_arch(tree, spec) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) log = None use_tree = (options.source == "tree" or \ (options.source == "any" and tree)) use_arch = (options.source == "archive" or options.source == "any") log = None if use_tree: for log in tree.iter_logs(revision.get_version()): if log.revision == revision: break else: log = None if log is None and use_arch: cmdutil.ensure_revision_exists(revision) log = arch.Patchlog(revision) if log is not None: for item in log.items(): print "%s: %s" % item print log.description def get_parser(self): """ Returns the options parser to use for the "cat-log" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai cat-log [revision]") parser.add_option("--archive", action="store_const", dest="source", const="archive", default="any", help="Always get the log from the archive") parser.add_option("--tree", action="store_const", dest="source", const="tree", help="Always get the log from the tree") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Prints the log for the specified revision """ help_tree_spec() return class Revert(BaseCommand): """ Reverts a tree (or aspects of it) to a revision """ def __init__(self): self.description="Reverts a tree (or aspects of it) to a revision " def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return iter_modified_file_completions(tree, arg) def do_command(self, cmdargs): """ Master function that perfoms the "revert" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: raise CommandFailed(e) spec=None if options.revision is not None: spec=options.revision try: if spec is not None: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = ancillary.comp_revision(tree) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) munger = None if options.file_contents or options.file_perms or options.deletions\ or options.additions or options.renames or options.hunk_prompt: munger = arch_compound.MungeOpts() munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm, options.hunk_prompt) if len(args) > 0 or options.logs or options.pattern_files or \ options.control: if munger is None: munger = cmdutil.arch_compound.MungeOpts(True) munger.all_types(True) if len(args) > 0: t_cwd = arch_compound.tree_cwd(tree) for name in args: if len(t_cwd) > 0: t_cwd += "/" name = "./" + t_cwd + name munger.add_keep_file(name); if options.file_perms: munger.file_perms = True if options.file_contents: munger.file_contents = True if options.deletions: munger.deletions = True if options.additions: munger.additions = True if options.renames: munger.renames = True if options.logs: munger.add_keep_pattern('^\./\{arch\}/[^=].*') if options.control: munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ "/\.arch-inventory$") if options.pattern_files: munger.add_keep_pattern(options.pattern_files) for line in arch_compound.revert(tree, revision, munger, not options.no_output): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "cat-log" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") parser.add_option("", "--contents", action="store_true", dest="file_contents", help="Revert file content changes") parser.add_option("", "--permissions", action="store_true", dest="file_perms", help="Revert file permissions changes") parser.add_option("", "--deletions", action="store_true", dest="deletions", help="Restore deleted files") parser.add_option("", "--additions", action="store_true", dest="additions", help="Remove added files") parser.add_option("", "--renames", action="store_true", dest="renames", help="Revert file names") parser.add_option("--hunks", action="store_true", dest="hunk_prompt", default=False, help="Prompt which hunks to revert") parser.add_option("--pattern-files", dest="pattern_files", help="Revert files that match this pattern", metavar="REGEX") parser.add_option("--logs", action="store_true", dest="logs", default=False, help="Revert only logs") parser.add_option("--control-files", action="store_true", dest="control", default=False, help="Revert logs and other control files") parser.add_option("-n", "--no-output", action="store_true", dest="no_output", help="Don't keep an undo changeset") parser.add_option("--revision", dest="revision", help="Revert to the specified revision", metavar="REVISION") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Reverts changes in the current working tree. If no flags are specified, all types of changes are reverted. Otherwise, only selected types of changes are reverted. If a revision is specified on the commandline, differences between the current tree and that revision are reverted. If a version is specified, the current tree is used to determine the revision. If files are specified, only those files listed will have any changes applied. To specify a renamed file, you can use either the old or new name. (or both!) Unless "-n" is specified, reversions can be undone with "redo". """ return class Revision(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Prints the name of a revision" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None spec=None if len(args) > 0: spec=args[0] if len(args) > 1: raise cmdutil.GetHelp try: if tree: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.determine_revision_arch(tree, spec) except cmdutil.CantDetermineRevision, e: print str(e) return print options.display(revision) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revision [revision]") parser.add_option("", "--location", action="store_const", const=paths.determine_path, dest="display", help="Show location instead of name", default=str) parser.add_option("--import", action="store_const", const=paths.determine_import_path, dest="display", help="Show location of import file") parser.add_option("--log", action="store_const", const=paths.determine_log_path, dest="display", help="Show location of log file") parser.add_option("--patch", action="store_const", dest="display", const=paths.determine_patch_path, help="Show location of patchfile") parser.add_option("--continuation", action="store_const", const=paths.determine_continuation_path, dest="display", help="Show location of continuation file") parser.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Expands aliases and prints the name of the specified revision. Instead of the name, several options can be used to print locations. If more than one is specified, the last one is used. """ help_tree_spec() return class Revisions(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Lists revisions" self.cl_revisions = [] def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ (options, args) = self.get_parser().parse_args(cmdargs) if len(args) > 1: raise cmdutil.GetHelp try: self.tree = arch.tree_root() except arch.errors.TreeRootError: self.tree = None if options.type == "default": options.type = "archive" try: iter = cmdutil.revision_iterator(self.tree, options.type, args, options.reverse, options.modified, options.shallow) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) except cmdutil.CantDetermineVersion, e: raise CommandFailedWrapper(e) if options.skip is not None: iter = cmdutil.iter_skip(iter, int(options.skip)) try: for revision in iter: log = None if isinstance(revision, arch.Patchlog): log = revision revision=revision.revision out = options.display(revision) if out is not None: print out if log is None and (options.summary or options.creator or options.date or options.merges): log = revision.patchlog if options.creator: print " %s" % log.creator if options.date: print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) if options.summary: print " %s" % log.summary if options.merges: showed_title = False for revision in log.merged_patches: if not showed_title: print " Merged:" showed_title = True print " %s" % revision if len(self.cl_revisions) > 0: print pylon.changelog_for_merge(self.cl_revisions) except pylon.errors.TreeRootNone: raise CommandFailedWrapper( Exception("This option can only be used in a project tree.")) def changelog_append(self, revision): if isinstance(revision, arch.Revision): revision=arch.Patchlog(revision) self.cl_revisions.append(revision) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revisions [version/revision]") select = cmdutil.OptionGroup(parser, "Selection options", "Control which revisions are listed. These options" " are mutually exclusive. If more than one is" " specified, the last is used.") cmdutil.add_revision_iter_options(select) parser.add_option("", "--skip", dest="skip", help="Skip revisions. Positive numbers skip from " "beginning, negative skip from end.", metavar="NUMBER") parser.add_option_group(select) format = cmdutil.OptionGroup(parser, "Revision format options", "These control the appearance of listed revisions") format.add_option("", "--location", action="store_const", const=paths.determine_path, dest="display", help="Show location instead of name", default=str) format.add_option("--import", action="store_const", const=paths.determine_import_path, dest="display", help="Show location of import file") format.add_option("--log", action="store_const", const=paths.determine_log_path, dest="display", help="Show location of log file") format.add_option("--patch", action="store_const", dest="display", const=paths.determine_patch_path, help="Show location of patchfile") format.add_option("--continuation", action="store_const", const=paths.determine_continuation_path, dest="display", help="Show location of continuation file") format.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") format.add_option("--changelog", action="store_const", const=self.changelog_append, dest="display", help="Show location of cacherev file") parser.add_option_group(format) display = cmdutil.OptionGroup(parser, "Display format options", "These control the display of data") display.add_option("-r", "--reverse", action="store_true", dest="reverse", help="Sort from newest to oldest") display.add_option("-s", "--summary", action="store_true", dest="summary", help="Show patchlog summary") display.add_option("-D", "--date", action="store_true", dest="date", help="Show patchlog date") display.add_option("-c", "--creator", action="store_true", dest="creator", help="Show the id that committed the" " revision") display.add_option("-m", "--merges", action="store_true", dest="merges", help="Show the revisions that were" " merged") parser.add_option_group(display) return parser def help(self, parser=None): """Attempt to explain the revisions command :param parser: If supplied, used to determine options """ if parser==None: parser=self.get_parser() parser.print_help() print """List revisions. """ help_tree_spec() class Get(BaseCommand): """ Retrieve a revision from the archive """ def __init__(self): self.description="Retrieve a revision from the archive" self.parser=self.get_parser() def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "get" command. """ (options, args) = self.parser.parse_args(cmdargs) if len(args) < 1: return self.help() try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None arch_loc = None try: revision, arch_loc = paths.full_path_decode(args[0]) except Exception, e: revision = cmdutil.determine_revision_arch(tree, args[0], check_existence=False, allow_package=True) if len(args) > 1: directory = args[1] else: directory = str(revision.nonarch) if os.path.exists(directory): raise DirectoryExists(directory) cmdutil.ensure_archive_registered(revision.archive, arch_loc) try: cmdutil.ensure_revision_exists(revision) except cmdutil.NoSuchRevision, e: raise CommandFailedWrapper(e) link = cmdutil.prompt ("get link") for line in cmdutil.iter_get(revision, directory, link, options.no_pristine, options.no_greedy_add): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "get" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai get revision [dir]") parser.add_option("--no-pristine", action="store_true", dest="no_pristine", help="Do not make pristine copy for reference") parser.add_option("--no-greedy-add", action="store_true", dest="no_greedy_add", help="Never add to greedy libraries") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Expands aliases and constructs a project tree for a revision. If the optional "dir" argument is provided, the project tree will be stored in this directory. """ help_tree_spec() return class PromptCmd(cmd.Cmd): def __init__(self): cmd.Cmd.__init__(self) self.prompt = "Fai> " try: self.tree = arch.tree_root() except: self.tree = None self.set_title() self.set_prompt() self.fake_aba = abacmds.AbaCmds() self.identchars += '-' self.history_file = os.path.expanduser("~/.fai-history") readline.set_completer_delims(string.whitespace) if os.access(self.history_file, os.R_OK) and \ os.path.isfile(self.history_file): readline.read_history_file(self.history_file) self.cwd = os.getcwd() def write_history(self): readline.write_history_file(self.history_file) def do_quit(self, args): self.write_history() sys.exit(0) def do_exit(self, args): self.do_quit(args) def do_EOF(self, args): print self.do_quit(args) def postcmd(self, line, bar): self.set_title() self.set_prompt() def set_prompt(self): if self.tree is not None: try: prompt = pylon.alias_or_version(self.tree.tree_version, self.tree, full=False) if prompt is not None: prompt = " " + prompt except: prompt = "" else: prompt = "" self.prompt = "Fai%s> " % prompt def set_title(self, command=None): try: version = pylon.alias_or_version(self.tree.tree_version, self.tree, full=False) except: version = "[no version]" if command is None: command = "" sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) def do_cd(self, line): if line == "": line = "~" line = os.path.expanduser(line) if os.path.isabs(line): newcwd = line else: newcwd = self.cwd+'/'+line newcwd = os.path.normpath(newcwd) try: os.chdir(newcwd) self.cwd = newcwd except Exception, e: print e try: self.tree = arch.tree_root() except: self.tree = None def do_help(self, line): Help()(line) def default(self, line): args = line.split() if find_command(args[0]): try: find_command(args[0]).do_command(args[1:]) except cmdutil.BadCommandOption, e: print e except cmdutil.GetHelp, e: find_command(args[0]).help() except CommandFailed, e: print e except arch.errors.ArchiveNotRegistered, e: print e except KeyboardInterrupt, e: print "Interrupted" except arch.util.ExecProblem, e: print e.proc.error.rstrip('\n') except cmdutil.CantDetermineVersion, e: print e except cmdutil.CantDetermineRevision, e: print e except Exception, e: print "Unhandled error:\n%s" % errors.exception_str(e) elif suggestions.has_key(args[0]): print suggestions[args[0]] elif self.fake_aba.is_command(args[0]): tree = None try: tree = arch.tree_root() except arch.errors.TreeRootError: pass cmd = self.fake_aba.is_command(args[0]) try: cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) except KeyboardInterrupt, e: print "Interrupted" elif options.tla_fallthrough and args[0] != "rm" and \ cmdutil.is_tla_command(args[0]): try: tree = None try: tree = arch.tree_root() except arch.errors.TreeRootError: pass args = cmdutil.expand_prefix_alias(args, tree) arch.util.exec_safe('tla', args, stderr=sys.stderr, expected=(0, 1)) except arch.util.ExecProblem, e: pass except KeyboardInterrupt, e: print "Interrupted" else: try: try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None args=line.split() os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) except KeyboardInterrupt, e: print "Interrupted" def completenames(self, text, line, begidx, endidx): completions = [] iter = iter_command_names(self.fake_aba) try: if len(line) > 0: arg = line.split()[-1] else: arg = "" iter = cmdutil.iter_munged_completions(iter, arg, text) except Exception, e: print e return list(iter) def completedefault(self, text, line, begidx, endidx): """Perform completion for native commands. :param text: The text to complete :type text: str :param line: The entire line to complete :type line: str :param begidx: The start of the text in the line :type begidx: int :param endidx: The end of the text in the line :type endidx: int """ try: (cmd, args, foo) = self.parseline(line) command_obj=find_command(cmd) if command_obj is not None: return command_obj.complete(args.split(), text) elif not self.fake_aba.is_command(cmd) and \ cmdutil.is_tla_command(cmd): iter = cmdutil.iter_supported_switches(cmd) if len(args) > 0: arg = args.split()[-1] else: arg = "" if arg.startswith("-"): return list(cmdutil.iter_munged_completions(iter, arg, text)) else: return list(cmdutil.iter_munged_completions( cmdutil.iter_file_completions(arg), arg, text)) elif cmd == "cd": if len(args) > 0: arg = args.split()[-1] else: arg = "" iter = cmdutil.iter_dir_completions(arg) iter = cmdutil.iter_munged_completions(iter, arg, text) return list(iter) elif len(args)>0: arg = args.split()[-1] iter = cmdutil.iter_file_completions(arg) return list(cmdutil.iter_munged_completions(iter, arg, text)) else: return self.completenames(text, line, begidx, endidx) except Exception, e: print e def iter_command_names(fake_aba): for entry in cmdutil.iter_combine([commands.iterkeys(), fake_aba.get_commands(), cmdutil.iter_tla_commands(False)]): if not suggestions.has_key(str(entry)): yield entry def iter_source_file_completions(tree, arg): treepath = arch_compound.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: dirs = None for file in tree.iter_inventory(dirs, source=True, both=True): file = file_completion_match(file, treepath, arg) if file is not None: yield file def iter_untagged(tree, dirs): for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, categories=arch_core.non_root, control_files=True): yield file.name def iter_untagged_completions(tree, arg): """Generate an iterator for all visible untagged files that match arg. :param tree: The tree to look for untagged files in :type tree: `arch.WorkingTree` :param arg: The argument to match :type arg: str :return: An iterator of all matching untagged files :rtype: iterator of str """ treepath = arch_compound.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: dirs = None for file in iter_untagged(tree, dirs): file = file_completion_match(file, treepath, arg) if file is not None: yield file def file_completion_match(file, treepath, arg): """Determines whether a file within an arch tree matches the argument. :param file: The rooted filename :type file: str :param treepath: The path to the cwd within the tree :type treepath: str :param arg: The prefix to match :return: The completion name, or None if not a match :rtype: str """ if not file.startswith(treepath): return None if treepath != "": file = file[len(treepath)+1:] if not file.startswith(arg): return None if os.path.isdir(file): file += '/' return file def iter_modified_file_completions(tree, arg): """Returns a list of modified files that match the specified prefix. :param tree: The current tree :type tree: `arch.WorkingTree` :param arg: The prefix to match :type arg: str """ treepath = arch_compound.tree_cwd(tree) tmpdir = util.tmpdir() changeset = tmpdir+"/changeset" completions = [] revision = cmdutil.determine_revision_tree(tree) for line in arch.iter_delta(revision, tree, changeset): if isinstance(line, arch.FileModification): file = file_completion_match(line.name[1:], treepath, arg) if file is not None: completions.append(file) shutil.rmtree(tmpdir) return completions class Shell(BaseCommand): def __init__(self): self.description = "Runs Fai as a shell" def do_command(self, cmdargs): if len(cmdargs)!=0: raise cmdutil.GetHelp prompt = PromptCmd() try: prompt.cmdloop() finally: prompt.write_history() class AddID(BaseCommand): """ Adds an inventory id for the given file """ def __init__(self): self.description="Add an inventory id for a given file" def get_completer(self, arg, index): tree = arch.tree_root() return iter_untagged_completions(tree, arg) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: raise pylon.errors.CommandFailedWrapper(e) if (len(args) == 0) == (options.untagged == False): raise cmdutil.GetHelp #if options.id and len(args) != 1: # print "If --id is specified, only one file can be named." # return method = tree.tagging_method if options.id_type == "tagline": if method != "tagline": if not cmdutil.prompt("Tagline in other tree"): if method == "explicit" or method == "implicit": options.id_type == method else: print "add-id not supported for \"%s\" tagging method"\ % method return elif options.id_type == "implicit": if method != "implicit": if not cmdutil.prompt("Implicit in other tree"): if method == "explicit" or method == "tagline": options.id_type == method else: print "add-id not supported for \"%s\" tagging method"\ % method return elif options.id_type == "explicit": if method != "tagline" and method != explicit: if not prompt("Explicit in other tree"): print "add-id not supported for \"%s\" tagging method" % \ method return if options.id_type == "auto": if method != "tagline" and method != "explicit" \ and method !="implicit": print "add-id not supported for \"%s\" tagging method" % method return else: options.id_type = method if options.untagged: args = None self.add_ids(tree, options.id_type, args) def add_ids(self, tree, id_type, files=()): """Add inventory ids to files. :param tree: the tree the files are in :type tree: `arch.WorkingTree` :param id_type: the type of id to add: "explicit" or "tagline" :type id_type: str :param files: The list of files to add. If None do all untagged. :type files: tuple of str """ untagged = (files is None) if untagged: files = list(iter_untagged(tree, None)) previous_files = [] while len(files) > 0: previous_files.extend(files) if id_type == "explicit": cmdutil.add_id(files) elif id_type == "tagline" or id_type == "implicit": for file in files: try: implicit = (id_type == "implicit") cmdutil.add_tagline_or_explicit_id(file, False, implicit) except cmdutil.AlreadyTagged: print "\"%s\" already has a tagline." % file except cmdutil.NoCommentSyntax: pass #do inventory after tagging until no untagged files are encountered if untagged: files = [] for file in iter_untagged(tree, None): if not file in previous_files: files.append(file) else: break def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") # ddaa suggests removing this to promote GUIDs. Let's see who squalks. # parser.add_option("-i", "--id", dest="id", # help="Specify id for a single file", default=None) parser.add_option("--tltl", action="store_true", dest="lord_style", help="Use Tom Lord's style of id.") parser.add_option("--explicit", action="store_const", const="explicit", dest="id_type", help="Use an explicit id", default="auto") parser.add_option("--tagline", action="store_const", const="tagline", dest="id_type", help="Use a tagline id") parser.add_option("--implicit", action="store_const", const="implicit", dest="id_type", help="Use an implicit id (deprecated)") parser.add_option("--untagged", action="store_true", dest="untagged", default=False, help="tag all untagged files") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Adds an inventory to the specified file(s) and directories. If --untagged is specified, adds inventory to all untagged files and directories. """ return class Merge(BaseCommand): """ Merges changes from other versions into the current tree """ def __init__(self): self.description="Merges changes from other versions" try: self.tree = arch.tree_root() except: self.tree = None def get_completer(self, arg, index): if self.tree is None: raise arch.errors.TreeRootError return cmdutil.merge_completions(self.tree, arg, index) def do_command(self, cmdargs): """ Master function that perfoms the "merge" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if options.diff3: action="star-merge" else: action = options.action if self.tree is None: raise arch.errors.TreeRootError(os.getcwd()) if cmdutil.has_changed(ancillary.comp_revision(self.tree)): raise UncommittedChanges(self.tree) if len(args) > 0: revisions = [] for arg in args: revisions.append(cmdutil.determine_revision_arch(self.tree, arg)) source = "from commandline" else: revisions = ancillary.iter_partner_revisions(self.tree, self.tree.tree_version) source = "from partner version" revisions = misc.rewind_iterator(revisions) try: revisions.next() revisions.rewind() except StopIteration, e: revision = cmdutil.tag_cur(self.tree) if revision is None: raise CantDetermineRevision("", "No version specified, no " "partner-versions, and no tag" " source") revisions = [revision] source = "from tag source" for revision in revisions: cmdutil.ensure_archive_registered(revision.archive) cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % (revision, source))) if action=="native-merge" or action=="update": if self.native_merge(revision, action) == 0: continue elif action=="star-merge": try: self.star_merge(revision, options.diff3) except errors.MergeProblem, e: break if cmdutil.has_changed(self.tree.tree_version): break def star_merge(self, revision, diff3): """Perform a star-merge on the current tree. :param revision: The revision to use for the merge :type revision: `arch.Revision` :param diff3: If true, do a diff3 merge :type diff3: bool """ try: for line in self.tree.iter_star_merge(revision, diff3=diff3): cmdutil.colorize(line) except arch.util.ExecProblem, e: if e.proc.status is not None and e.proc.status == 1: if e.proc.error: print e.proc.error raise MergeProblem else: raise def native_merge(self, other_revision, action): """Perform a native-merge on the current tree. :param other_revision: The revision to use for the merge :type other_revision: `arch.Revision` :return: 0 if the merge was skipped, 1 if it was applied """ other_tree = arch_compound.find_or_make_local_revision(other_revision) try: if action == "native-merge": ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, other_revision) elif action == "update": ancestor = arch_compound.tree_latest(self.tree, other_revision.version) except CantDetermineRevision, e: raise CommandFailedWrapper(e) cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) if (ancestor == other_revision): cmdutil.colorize(arch.Chatter("* Skipping redundant merge" % ancestor)) return 0 delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) for line in cmdutil.iter_apply_delta_filter(delta): cmdutil.colorize(line) return 1 def get_parser(self): """ Returns the options parser to use for the "merge" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai merge [VERSION]") parser.add_option("-s", "--star-merge", action="store_const", dest="action", help="Use star-merge", const="star-merge", default="native-merge") parser.add_option("--update", action="store_const", dest="action", help="Use update picker", const="update") parser.add_option("--diff3", action="store_true", dest="diff3", help="Use diff3 for merge (implies star-merge)") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Performs a merge operation using the specified version. """ return class ELog(BaseCommand): """ Produces a raw patchlog and invokes the user's editor """ def __init__(self): self.description="Edit a patchlog to commit" try: self.tree = arch.tree_root() except: self.tree = None def do_command(self, cmdargs): """ Master function that perfoms the "elog" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if self.tree is None: raise arch.errors.TreeRootError try: edit_log(self.tree, self.tree.tree_version) except pylon.errors.NoEditorSpecified, e: raise pylon.errors.CommandFailedWrapper(e) def get_parser(self): """ Returns the options parser to use for the "merge" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai elog") return parser def help(self, parser=None): """ Invokes $EDITOR to produce a log for committing. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Invokes $EDITOR to produce a log for committing. """ return def edit_log(tree, version): """Makes and edits the log for a tree. Does all kinds of fancy things like log templates and merge summaries and log-for-merge :param tree: The tree to edit the log for :type tree: `arch.WorkingTree` """ #ensure we have an editor before preparing the log cmdutil.find_editor() log = tree.log_message(create=False, version=version) log_is_new = False if log is None or cmdutil.prompt("Overwrite log"): if log is not None: os.remove(log.name) log = tree.log_message(create=True, version=version) log_is_new = True tmplog = log.name template = pylon.log_template_path(tree) if template: shutil.copyfile(template, tmplog) comp_version = ancillary.comp_revision(tree).version new_merges = cmdutil.iter_new_merges(tree, comp_version) new_merges = cmdutil.direct_merges(new_merges) log["Summary"] = pylon.merge_summary(new_merges, version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): if cmdutil.prompt("changelog for merge"): mergestuff = "Patches applied:\n" mergestuff += pylon.changelog_for_merge(new_merges) else: mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: cmdutil.invoke_editor(log.name) except: if log_is_new: os.remove(log.name) raise class MirrorArchive(BaseCommand): """ Updates a mirror from an archive """ def __init__(self): self.description="Update a mirror from an archive" def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if len(args) > 1: raise GetHelp try: tree = arch.tree_root() except: tree = None if len(args) == 0: if tree is not None: name = tree.tree_version() else: name = cmdutil.expand_alias(args[0], tree) name = arch.NameParser(name) to_arch = name.get_archive() from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) limit = name.get_nonarch() iter = arch_core.mirror_archive(from_arch,to_arch, limit) for line in arch.chatter_classifier(iter): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Updates a mirror from an archive. If a branch, package, or version is supplied, only changes under it are mirrored. """ return def help_tree_spec(): print """Specifying revisions (default: tree) Revisions may be specified by alias, revision, version or patchlevel. Revisions or versions may be fully qualified. Unqualified revisions, versions, or patchlevels use the archive of the current project tree. Versions will use the latest patchlevel in the tree. Patchlevels will use the current tree- version. Use "alias" to list available (user and automatic) aliases.""" auto_alias = [ "acur", "The latest revision in the archive of the tree-version. You can specify \ a different version like so: acur:foo--bar--0 (aliases can be used)", "tcur", """(tree current) The latest revision in the tree of the tree-version. \ You can specify a different version like so: tcur:foo--bar--0 (aliases can be \ used).""", "tprev" , """(tree previous) The previous revision in the tree of the tree-version. To \ specify an older revision, use a number, e.g. "tprev:4" """, "tanc" , """(tree ancestor) The ancestor revision of the tree To specify an older \ revision, use a number, e.g. "tanc:4".""", "tdate" , """(tree date) The latest revision from a given date, e.g. "tdate:July 6".""", "tmod" , """ (tree modified) The latest revision to modify a given file, e.g. \ "tmod:engine.cpp" or "tmod:engine.cpp:16".""", "ttag" , """(tree tag) The revision that was tagged into the current tree revision, \ according to the tree""", "tagcur", """(tag current) The latest revision of the version that the current tree \ was tagged from.""", "mergeanc" , """The common ancestor of the current tree and the specified revision. \ Defaults to the first partner-version's latest revision or to tagcur.""", ] def is_auto_alias(name): """Determine whether a name is an auto alias name :param name: the name to check :type name: str :return: True if the name is an auto alias, false if not :rtype: bool """ return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)] def display_def(iter, wrap = 80): """Display a list of definitions :param iter: iter of name, definition pairs :type iter: iter of (str, str) :param wrap: The width for text wrapping :type wrap: int """ vals = list(iter) maxlen = 0 for (key, value) in vals: if len(key) > maxlen: maxlen = len(key) for (key, value) in vals: tw=textwrap.TextWrapper(width=wrap, initial_indent=key.rjust(maxlen)+" : ", subsequent_indent="".rjust(maxlen+3)) print tw.fill(value) def help_aliases(tree): print """Auto-generated aliases""" display_def(pylon.util.iter_pairs(auto_alias)) print "User aliases" display_def(ancillary.iter_all_alias(tree)) class Inventory(BaseCommand): """List the status of files in the tree""" def __init__(self): self.description=self.__doc__ def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) tree = arch.tree_root() categories = [] if (options.source): categories.append(arch_core.SourceFile) if (options.precious): categories.append(arch_core.PreciousFile) if (options.backup): categories.append(arch_core.BackupFile) if (options.junk): categories.append(arch_core.JunkFile) if len(categories) == 1: show_leading = False else: show_leading = True if len(categories) == 0: categories = None if options.untagged: categories = arch_core.non_root show_leading = False tagged = False else: tagged = None for file in arch_core.iter_inventory_filter(tree, None, control_files=options.control_files, categories = categories, tagged=tagged): print arch_core.file_line(file, category = show_leading, untagged = show_leading, id = options.ids) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai inventory [options]") parser.add_option("--ids", action="store_true", dest="ids", help="Show file ids") parser.add_option("--control", action="store_true", dest="control_files", help="include control files") parser.add_option("--source", action="store_true", dest="source", help="List source files") parser.add_option("--backup", action="store_true", dest="backup", help="List backup files") parser.add_option("--precious", action="store_true", dest="precious", help="List precious files") parser.add_option("--junk", action="store_true", dest="junk", help="List junk files") parser.add_option("--unrecognized", action="store_true", dest="unrecognized", help="List unrecognized files") parser.add_option("--untagged", action="store_true", dest="untagged", help="List only untagged files") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Lists the status of files in the archive: S source P precious B backup J junk U unrecognized T tree root ? untagged-source Leading letter are not displayed if only one kind of file is shown """ return class Alias(BaseCommand): """List or adjust aliases""" def __init__(self): self.description=self.__doc__ def get_completer(self, arg, index): if index > 2: return () try: self.tree = arch.tree_root() except: self.tree = None if index == 0: return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] elif index == 1: return cmdutil.iter_revision_completions(arg, self.tree) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: self.tree = arch.tree_root() except: self.tree = None try: options.action(args, options) except cmdutil.ForbiddenAliasSyntax, e: raise CommandFailedWrapper(e) def no_prefix(self, alias): if alias.startswith("^"): alias = alias[1:] return alias def arg_dispatch(self, args, options): """Add, modify, or list aliases, depending on number of arguments :param args: The list of commandline arguments :type args: list of str :param options: The commandline options """ if len(args) == 0: help_aliases(self.tree) return else: alias = self.no_prefix(args[0]) if len(args) == 1: self.print_alias(alias) elif (len(args)) == 2: self.add(alias, args[1], options) else: raise cmdutil.GetHelp def print_alias(self, alias): answer = None if is_auto_alias(alias): raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias." " Use \"revision\" to expand auto aliases." % alias) for pair in ancillary.iter_all_alias(self.tree): if pair[0] == alias: answer = pair[1] if answer is not None: print answer else: print "The alias %s is not assigned." % alias def add(self, alias, expansion, options): """Add or modify aliases :param alias: The alias name to create/modify :type alias: str :param expansion: The expansion to assign to the alias name :type expansion: str :param options: The commandline options """ if is_auto_alias(alias): raise IsAutoAlias(alias) newlist = "" written = False new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, self.tree)) ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) for pair in self.get_iterator(options): if pair[0] != alias: newlist+="%s=%s\n" % (pair[0], pair[1]) elif not written: newlist+=new_line written = True if not written: newlist+=new_line self.write_aliases(newlist, options) def delete(self, args, options): """Delete the specified alias :param args: The list of arguments :type args: list of str :param options: The commandline options """ deleted = False if len(args) != 1: raise cmdutil.GetHelp alias = self.no_prefix(args[0]) if is_auto_alias(alias): raise IsAutoAlias(alias) newlist = "" for pair in self.get_iterator(options): if pair[0] != alias: newlist+="%s=%s\n" % (pair[0], pair[1]) else: deleted = True if not deleted: raise errors.NoSuchAlias(alias) self.write_aliases(newlist, options) def get_alias_file(self, options): """Return the name of the alias file to use :param options: The commandline options """ if options.tree: if self.tree is None: self.tree == arch.tree_root() return str(self.tree)+"/{arch}/+aliases" else: return "~/.aba/aliases" def get_iterator(self, options): """Return the alias iterator to use :param options: The commandline options """ return ancillary.iter_alias(self.get_alias_file(options)) def write_aliases(self, newlist, options): """Safely rewrite the alias file :param newlist: The new list of aliases :type newlist: str :param options: The commandline options """ filename = os.path.expanduser(self.get_alias_file(options)) file = util.NewFileVersion(filename) file.write(newlist) file.commit() def get_parser(self): """ Returns the options parser to use for the "alias" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") parser.add_option("-d", "--delete", action="store_const", dest="action", const=self.delete, default=self.arg_dispatch, help="Delete an alias") parser.add_option("--tree", action="store_true", dest="tree", help="Create a per-tree alias", default=False) return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Lists current aliases or modifies the list of aliases. If no arguments are supplied, aliases will be listed. If two arguments are supplied, the specified alias will be created or modified. If -d or --delete is supplied, the specified alias will be deleted. You can create aliases that refer to any fully-qualified part of the Arch namespace, e.g. archive, archive/category, archive/category--branch, archive/category--branch--version (my favourite) archive/category--branch--version--patchlevel Aliases can be used automatically by native commands. To use them with external or tla commands, prefix them with ^ (you can do this with native commands, too). """ class RequestMerge(BaseCommand): """Submit a merge request to Bug Goo""" def __init__(self): self.description=self.__doc__ def do_command(self, cmdargs): """Submit a merge request :param cmdargs: The commandline arguments :type cmdargs: list of str """ parser = self.get_parser() (options, args) = parser.parse_args(cmdargs) try: cmdutil.find_editor() except pylon.errors.NoEditorSpecified, e: raise pylon.errors.CommandFailedWrapper(e) try: self.tree=arch.tree_root() except: self.tree=None base, revisions = self.revision_specs(args) message = self.make_headers(base, revisions) message += self.make_summary(revisions) path = self.edit_message(message) message = self.tidy_message(path) if cmdutil.prompt("Send merge"): self.send_message(message) print "Merge request sent" def make_headers(self, base, revisions): """Produce email and Bug Goo header strings :param base: The base revision to apply merges to :type base: `arch.Revision` :param revisions: The revisions to replay into the base :type revisions: list of `arch.Patchlog` :return: The headers :rtype: str """ headers = "To: gnu-arch-users@gnu.org\n" headers += "From: %s\n" % options.fromaddr if len(revisions) == 1: headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary else: headers += "Subject: [MERGE REQUEST]\n" headers += "\n" headers += "Base-Revision: %s\n" % base for revision in revisions: headers += "Revision: %s\n" % revision.revision headers += "Bug: \n\n" return headers def make_summary(self, logs): """Generate a summary of merges :param logs: the patchlogs that were directly added by the merges :type logs: list of `arch.Patchlog` :return: the summary :rtype: str """ summary = "" for log in logs: summary+=str(log.revision)+"\n" summary+=log.summary+"\n" if log.description.strip(): summary+=log.description.strip('\n')+"\n\n" return summary def revision_specs(self, args): """Determine the base and merge revisions from tree and arguments. :param args: The parsed arguments :type args: list of str :return: The base revision and merge revisions :rtype: `arch.Revision`, list of `arch.Patchlog` """ if len(args) > 0: target_revision = cmdutil.determine_revision_arch(self.tree, args[0]) else: target_revision = arch_compound.tree_latest(self.tree) if len(args) > 1: merges = [ arch.Patchlog(cmdutil.determine_revision_arch( self.tree, f)) for f in args[1:] ] else: if self.tree is None: raise CantDetermineRevision("", "Not in a project tree") merge_iter = cmdutil.iter_new_merges(self.tree, target_revision.version, False) merges = [f for f in cmdutil.direct_merges(merge_iter)] return (target_revision, merges) def edit_message(self, message): """Edit an email message in the user's standard editor :param message: The message to edit :type message: str :return: the path of the edited message :rtype: str """ if self.tree is None: path = os.get_cwd() else: path = self.tree path += "/,merge-request" file = open(path, 'w') file.write(message) file.flush() cmdutil.invoke_editor(path) return path def tidy_message(self, path): """Validate and clean up message. :param path: The path to the message to clean up :type path: str :return: The parsed message :rtype: `email.Message` """ mail = email.message_from_file(open(path)) if mail["Subject"].strip() == "[MERGE REQUEST]": raise BlandSubject request = email.message_from_string(mail.get_payload()) if request.has_key("Bug"): if request["Bug"].strip()=="": del request["Bug"] mail.set_payload(request.as_string()) return mail def send_message(self, message): """Send a message, using its headers to address it. :param message: The message to send :type message: `email.Message`""" server = smtplib.SMTP("localhost") server.sendmail(message['From'], message['To'], message.as_string()) server.quit() def help(self, parser=None): """Print a usage message :param parser: The options parser to use :type parser: `cmdutil.CmdOptionParser` """ if parser is None: parser = self.get_parser() parser.print_help() print """ Sends a merge request formatted for Bug Goo. Intended use: get the tree you'd like to merge into. Apply the merges you want. Invoke request-merge. The merge request will open in your $EDITOR. When no TARGET is specified, it uses the current tree revision. When no MERGE is specified, it uses the direct merges (as in "revisions --direct-merges"). But you can specify just the TARGET, or all the MERGE revisions. """ def get_parser(self): """Produce a commandline parser for this command. :rtype: `cmdutil.CmdOptionParser` """ parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") return parser commands = { 'changes' : Changes, 'help' : Help, 'update': Update, 'apply-changes':ApplyChanges, 'cat-log': CatLog, 'commit': Commit, 'revision': Revision, 'revisions': Revisions, 'get': Get, 'revert': Revert, 'shell': Shell, 'add-id': AddID, 'merge': Merge, 'elog': ELog, 'mirror-archive': MirrorArchive, 'ninventory': Inventory, 'alias' : Alias, 'request-merge': RequestMerge, } def my_import(mod_name): module = __import__(mod_name) components = mod_name.split('.') for comp in components[1:]: module = getattr(module, comp) return module def plugin(mod_name): module = my_import(mod_name) module.add_command(commands) for file in os.listdir(sys.path[0]+"/command"): if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py": plugin("command."+file[:-3]) suggestions = { 'apply-delta' : "Try \"apply-changes\".", 'delta' : "To compare two revisions, use \"changes\".", 'diff-rev' : "To compare two revisions, use \"changes\".", 'undo' : "To undo local changes, use \"revert\".", 'undelete' : "To undo only deletions, use \"revert --deletions\"", 'missing-from' : "Try \"revisions --missing-from\".", 'missing' : "Try \"revisions --missing\".", 'missing-merge' : "Try \"revisions --partner-missing\".", 'new-merges' : "Try \"revisions --new-merges\".", 'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", 'logs' : "Try \"revisions --logs\"", 'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", 'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", 'change-version' : "Try \"update REVISION\"", 'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", 'rev-depends' : "Use revisions --dependencies", 'auto-get' : "Plain get will do archive lookups", 'tagline' : "Use add-id. It uses taglines in tagline trees", 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", 'library-revisions' : "Use revisions --library", 'file-revert' : "Use revert FILE", 'join-branch' : "Use replay --logs-only" } # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 patchkit-0.1.8/test_patches_data/mod-2000064400000000000000000000441611046102023000157670ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 for line in orig_lines: yield line import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.1.8/test_patches_data/mod-3000064400000000000000000000442031046102023000157650ustar 00000000000000First line change # Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 for line in orig_lines: yield line import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.1.8/test_patches_data/mod-4000064400000000000000000000440421046102023000157670ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() last line change patchkit-0.1.8/test_patches_data/mod-5000064400000000000000000000331241046102023000157670ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.1.8/test_patches_data/mod-6000064400000000000000000000000261046102023000157630ustar 00000000000000Total contents change patchkit-0.1.8/test_patches_data/mod-7000064400000000000000000000000211046102023000157570ustar 00000000000000No newline eitherpatchkit-0.1.8/test_patches_data/orig000064400000000000000000003036341046102023000160140ustar 00000000000000# Copyright (C) 2004 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import sys import arch import arch.util import arch.arch import abacmds import cmdutil import shutil import os import options import paths import time import cmd import readline import re import string import arch_core from errors import * import errors import terminal import ancillary import misc import email import smtplib __docformat__ = "restructuredtext" __doc__ = "Implementation of user (sub) commands" commands = {} def find_command(cmd): """ Return an instance of a command type. Return None if the type isn't registered. :param cmd: the name of the command to look for :type cmd: the type of the command """ if commands.has_key(cmd): return commands[cmd]() else: return None class BaseCommand: def __call__(self, cmdline): try: self.do_command(cmdline.split()) except cmdutil.GetHelp, e: self.help() except Exception, e: print e def get_completer(index): return None def complete(self, args, text): """ Returns a list of possible completions for the given text. :param args: The complete list of arguments :type args: List of str :param text: text to complete (may be shorter than args[-1]) :type text: str :rtype: list of str """ matches = [] candidates = None if len(args) > 0: realtext = args[-1] else: realtext = "" try: parser=self.get_parser() if realtext.startswith('-'): candidates = parser.iter_options() else: (options, parsed_args) = parser.parse_args(args) if len (parsed_args) > 0: candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1) else: candidates = self.get_completer("", 0) except: pass if candidates is None: return for candidate in candidates: candidate = str(candidate) if candidate.startswith(realtext): matches.append(candidate[len(realtext)- len(text):]) return matches class Help(BaseCommand): """ Lists commands, prints help messages. """ def __init__(self): self.description="Prints help mesages" self.parser = None def do_command(self, cmdargs): """ Prints a help message. """ options, args = self.get_parser().parse_args(cmdargs) if len(args) > 1: raise cmdutil.GetHelp if options.native or options.suggestions or options.external: native = options.native suggestions = options.suggestions external = options.external else: native = True suggestions = False external = True if len(args) == 0: self.list_commands(native, suggestions, external) return elif len(args) == 1: command_help(args[0]) return def help(self): self.get_parser().print_help() print """ If no command is specified, commands are listed. If a command is specified, help for that command is listed. """ def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ if self.parser is not None: return self.parser parser=cmdutil.CmdOptionParser("fai help [command]") parser.add_option("-n", "--native", action="store_true", dest="native", help="Show native commands") parser.add_option("-e", "--external", action="store_true", dest="external", help="Show external commands") parser.add_option("-s", "--suggest", action="store_true", dest="suggestions", help="Show suggestions") self.parser = parser return parser def list_commands(self, native=True, suggest=False, external=True): """ Lists supported commands. :param native: list native, python-based commands :type native: bool :param external: list external aba-style commands :type external: bool """ if native: print "Native Fai commands" keys=commands.keys() keys.sort() for k in keys: space="" for i in range(28-len(k)): space+=" " print space+k+" : "+commands[k]().description print if suggest: print "Unavailable commands and suggested alternatives" key_list = suggestions.keys() key_list.sort() for key in key_list: print "%28s : %s" % (key, suggestions[key]) print if external: fake_aba = abacmds.AbaCmds() if (fake_aba.abadir == ""): return print "External commands" fake_aba.list_commands() print if not suggest: print "Use help --suggest to list alternatives to tla and aba"\ " commands." if options.tla_fallthrough and (native or external): print "Fai also supports tla commands." def command_help(cmd): """ Prints help for a command. :param cmd: The name of the command to print help for :type cmd: str """ fake_aba = abacmds.AbaCmds() cmdobj = find_command(cmd) if cmdobj != None: cmdobj.help() elif suggestions.has_key(cmd): print "Not available\n" + suggestions[cmd] else: abacmd = fake_aba.is_command(cmd) if abacmd: abacmd.help() else: print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?" class Changes(BaseCommand): """ the "changes" command: lists differences between trees/revisions: """ def __init__(self): self.description="Lists what files have changed in the project tree" def get_completer(self, arg, index): if index > 1: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) > 2: raise cmdutil.GetHelp tree=arch.tree_root() if len(args) == 0: a_spec = cmdutil.comp_revision(tree) else: a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) if len(args) == 2: b_spec = cmdutil.determine_revision_tree(tree, args[1]) cmdutil.ensure_archive_registered(b_spec.archive) else: b_spec=tree return options, a_spec, b_spec def do_command(self, cmdargs): """ Master function that perfoms the "changes" command. """ try: options, a_spec, b_spec = self.parse_commandline(cmdargs); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return if options.changeset: changeset=options.changeset tmpdir = None else: tmpdir=cmdutil.tmpdir() changeset=tmpdir+"/changeset" try: delta=arch.iter_delta(a_spec, b_spec, changeset) try: for line in delta: if cmdutil.chattermatch(line, "changeset:"): pass else: cmdutil.colorize(line, options.suppress_chatter) except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "missing explicit id for file"): raise MissingID(e) else: raise status=delta.status if status > 1: return if (options.perform_diff): chan = cmdutil.ChangesetMunger(changeset) chan.read_indices() if isinstance(b_spec, arch.Revision): b_dir = b_spec.library_find() else: b_dir = b_spec a_dir = a_spec.library_find() if options.diffopts is not None: diffopts = options.diffopts.split() cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir) else: cmdutil.show_diffs(delta.changeset) finally: if tmpdir and (os.access(tmpdir, os.X_OK)): shutil.rmtree(tmpdir) def get_parser(self): """ Returns the options parser to use for the "changes" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai changes [options] [revision]" " [revision]") parser.add_option("-d", "--diff", action="store_true", dest="perform_diff", default=False, help="Show diffs in summary") parser.add_option("-c", "--changeset", dest="changeset", help="Store a changeset in the given directory", metavar="DIRECTORY") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") parser.add_option("--diffopts", dest="diffopts", help="Use the specified diff options", metavar="OPTIONS") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Performs source-tree comparisons If no revision is specified, the current project tree is compared to the last-committed revision. If one revision is specified, the current project tree is compared to that revision. If two revisions are specified, they are compared to each other. """ help_tree_spec() return class ApplyChanges(BaseCommand): """ Apply differences between two revisions to a tree """ def __init__(self): self.description="Applies changes to a project tree" def get_completer(self, arg, index): if index > 1: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) != 2: raise cmdutil.GetHelp a_spec = cmdutil.determine_revision_tree(tree, args[0]) cmdutil.ensure_archive_registered(a_spec.archive) b_spec = cmdutil.determine_revision_tree(tree, args[1]) cmdutil.ensure_archive_registered(b_spec.archive) return options, a_spec, b_spec def do_command(self, cmdargs): """ Master function that performs "apply-changes". """ try: tree = arch.tree_root() options, a_spec, b_spec = self.parse_commandline(cmdargs, tree); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return delta=cmdutil.apply_delta(a_spec, b_spec, tree) for line in cmdutil.iter_apply_delta_filter(delta): cmdutil.colorize(line, options.suppress_chatter) def get_parser(self): """ Returns the options parser to use for the "apply-changes" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision" " revision") parser.add_option("-d", "--diff", action="store_true", dest="perform_diff", default=False, help="Show diffs in summary") parser.add_option("-c", "--changeset", dest="changeset", help="Store a changeset in the given directory", metavar="DIRECTORY") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Applies changes to a project tree Compares two revisions and applies the difference between them to the current tree. """ help_tree_spec() return class Update(BaseCommand): """ Updates a project tree to a given revision, preserving un-committed hanges. """ def __init__(self): self.description="Apply the latest changes to the current directory" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raises cmdutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) > 2: raise cmdutil.GetHelp spec=None if len(args)>0: spec=args[0] revision=cmdutil.determine_revision_arch(tree, spec) cmdutil.ensure_archive_registered(revision.archive) mirror_source = cmdutil.get_mirror_source(revision.archive) if mirror_source != None: if cmdutil.prompt("Mirror update"): cmd=cmdutil.mirror_archive(mirror_source, revision.archive, arch.NameParser(revision).get_package_version()) for line in arch.chatter_classifier(cmd): cmdutil.colorize(line, options.suppress_chatter) revision=cmdutil.determine_revision_arch(tree, spec) return options, revision def do_command(self, cmdargs): """ Master function that perfoms the "update" command. """ tree=arch.tree_root() try: options, to_revision = self.parse_commandline(cmdargs, tree); except cmdutil.CantDetermineRevision, e: print e return except arch.errors.TreeRootError, e: print e return from_revision=cmdutil.tree_latest(tree) if from_revision==to_revision: print "Tree is already up to date with:\n"+str(to_revision)+"." return cmdutil.ensure_archive_registered(from_revision.archive) cmd=cmdutil.apply_delta(from_revision, to_revision, tree, options.patch_forward) for line in cmdutil.iter_apply_delta_filter(cmd): cmdutil.colorize(line) if to_revision.version != tree.tree_version: if cmdutil.prompt("Update version"): tree.tree_version = to_revision.version def get_parser(self): """ Returns the options parser to use for the "update" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai update [options]" " [revision/version]") parser.add_option("-f", "--forward", action="store_true", dest="patch_forward", default=False, help="pass the --forward option to 'patch'") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Updates a working tree to the current archive revision If a revision or version is specified, that is used instead """ help_tree_spec() return class Commit(BaseCommand): """ Create a revision based on the changes in the current tree. """ def __init__(self): self.description="Write local changes to the archive" def get_completer(self, arg, index): if arg is None: arg = "" return iter_modified_file_completions(arch.tree_root(), arg) # return iter_source_file_completions(arch.tree_root(), arg) def parse_commandline(self, cmdline, tree): """ Parse commandline arguments. Raise cmtutil.GetHelp if help is needed. :param cmdline: A list of arguments to parse :rtype: (options, Revision, Revision/WorkingTree) """ parser=self.get_parser() (options, args) = parser.parse_args(cmdline) if len(args) == 0: args = None revision=cmdutil.determine_revision_arch(tree, options.version) return options, revision.get_version(), args def do_command(self, cmdargs): """ Master function that perfoms the "commit" command. """ tree=arch.tree_root() options, version, files = self.parse_commandline(cmdargs, tree) if options.__dict__.has_key("base") and options.base: base = cmdutil.determine_revision_tree(tree, options.base) else: base = cmdutil.submit_revision(tree) writeversion=version archive=version.archive source=cmdutil.get_mirror_source(archive) allow_old=False writethrough="implicit" if source!=None: if writethrough=="explicit" and \ cmdutil.prompt("Writethrough"): writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch())) elif writethrough=="none": raise CommitToMirror(archive) elif archive.is_mirror: raise CommitToMirror(archive) try: last_revision=tree.iter_logs(version, True).next().revision except StopIteration, e: if cmdutil.prompt("Import from commit"): return do_import(version) else: raise NoVersionLogs(version) if last_revision!=version.iter_revisions(True).next(): if not cmdutil.prompt("Out of date"): raise OutOfDate else: allow_old=True try: if not cmdutil.has_changed(version): if not cmdutil.prompt("Empty commit"): raise EmptyCommit except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "missing explicit id for file"): raise MissingID(e) else: raise log = tree.log_message(create=False) if log is None: try: if cmdutil.prompt("Create log"): edit_log(tree) except cmdutil.NoEditorSpecified, e: raise CommandFailed(e) log = tree.log_message(create=False) if log is None: raise NoLogMessage if log["Summary"] is None or len(log["Summary"].strip()) == 0: if not cmdutil.prompt("Omit log summary"): raise errors.NoLogSummary try: for line in tree.iter_commit(version, seal=options.seal_version, base=base, out_of_date_ok=allow_old, file_list=files): cmdutil.colorize(line, options.suppress_chatter) except arch.util.ExecProblem, e: if e.proc.error and e.proc.error.startswith( "These files violate naming conventions:"): raise LintFailure(e.proc.error) else: raise def get_parser(self): """ Returns the options parser to use for the "commit" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai commit [options] [file1]" " [file2...]") parser.add_option("--seal", action="store_true", dest="seal_version", default=False, help="seal this version") parser.add_option("-v", "--version", dest="version", help="Use the specified version", metavar="VERSION") parser.add_option("-s", "--silent", action="store_true", dest="suppress_chatter", default=False, help="Suppress chatter messages") if cmdutil.supports_switch("commit", "--base"): parser.add_option("--base", dest="base", help="", metavar="REVISION") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser is None: parser=self.get_parser() parser.print_help() print """ Updates a working tree to the current archive revision If a version is specified, that is used instead """ # help_tree_spec() return class CatLog(BaseCommand): """ Print the log of a given file (from current tree) """ def __init__(self): self.description="Prints the patch log for a revision" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "cat-log" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: tree = None spec=None if len(args) > 0: spec=args[0] if len(args) > 1: raise cmdutil.GetHelp() try: if tree: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.determine_revision_arch(tree, spec) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) log = None use_tree = (options.source == "tree" or \ (options.source == "any" and tree)) use_arch = (options.source == "archive" or options.source == "any") log = None if use_tree: for log in tree.iter_logs(revision.get_version()): if log.revision == revision: break else: log = None if log is None and use_arch: cmdutil.ensure_revision_exists(revision) log = arch.Patchlog(revision) if log is not None: for item in log.items(): print "%s: %s" % item print log.description def get_parser(self): """ Returns the options parser to use for the "cat-log" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai cat-log [revision]") parser.add_option("--archive", action="store_const", dest="source", const="archive", default="any", help="Always get the log from the archive") parser.add_option("--tree", action="store_const", dest="source", const="tree", help="Always get the log from the tree") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Prints the log for the specified revision """ help_tree_spec() return class Revert(BaseCommand): """ Reverts a tree (or aspects of it) to a revision """ def __init__(self): self.description="Reverts a tree (or aspects of it) to a revision " def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return iter_modified_file_completions(tree, arg) def do_command(self, cmdargs): """ Master function that perfoms the "revert" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError, e: raise CommandFailed(e) spec=None if options.revision is not None: spec=options.revision try: if spec is not None: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.comp_revision(tree) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) munger = None if options.file_contents or options.file_perms or options.deletions\ or options.additions or options.renames or options.hunk_prompt: munger = cmdutil.MungeOpts() munger.hunk_prompt = options.hunk_prompt if len(args) > 0 or options.logs or options.pattern_files or \ options.control: if munger is None: munger = cmdutil.MungeOpts(True) munger.all_types(True) if len(args) > 0: t_cwd = cmdutil.tree_cwd(tree) for name in args: if len(t_cwd) > 0: t_cwd += "/" name = "./" + t_cwd + name munger.add_keep_file(name); if options.file_perms: munger.file_perms = True if options.file_contents: munger.file_contents = True if options.deletions: munger.deletions = True if options.additions: munger.additions = True if options.renames: munger.renames = True if options.logs: munger.add_keep_pattern('^\./\{arch\}/[^=].*') if options.control: munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\ "/\.arch-inventory$") if options.pattern_files: munger.add_keep_pattern(options.pattern_files) for line in cmdutil.revert(tree, revision, munger, not options.no_output): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "cat-log" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]") parser.add_option("", "--contents", action="store_true", dest="file_contents", help="Revert file content changes") parser.add_option("", "--permissions", action="store_true", dest="file_perms", help="Revert file permissions changes") parser.add_option("", "--deletions", action="store_true", dest="deletions", help="Restore deleted files") parser.add_option("", "--additions", action="store_true", dest="additions", help="Remove added files") parser.add_option("", "--renames", action="store_true", dest="renames", help="Revert file names") parser.add_option("--hunks", action="store_true", dest="hunk_prompt", default=False, help="Prompt which hunks to revert") parser.add_option("--pattern-files", dest="pattern_files", help="Revert files that match this pattern", metavar="REGEX") parser.add_option("--logs", action="store_true", dest="logs", default=False, help="Revert only logs") parser.add_option("--control-files", action="store_true", dest="control", default=False, help="Revert logs and other control files") parser.add_option("-n", "--no-output", action="store_true", dest="no_output", help="Don't keep an undo changeset") parser.add_option("--revision", dest="revision", help="Revert to the specified revision", metavar="REVISION") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Reverts changes in the current working tree. If no flags are specified, all types of changes are reverted. Otherwise, only selected types of changes are reverted. If a revision is specified on the commandline, differences between the current tree and that revision are reverted. If a version is specified, the current tree is used to determine the revision. If files are specified, only those files listed will have any changes applied. To specify a renamed file, you can use either the old or new name. (or both!) Unless "-n" is specified, reversions can be undone with "redo". """ return class Revision(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Prints the name of a revision" def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None spec=None if len(args) > 0: spec=args[0] if len(args) > 1: raise cmdutil.GetHelp try: if tree: revision = cmdutil.determine_revision_tree(tree, spec) else: revision = cmdutil.determine_revision_arch(tree, spec) except cmdutil.CantDetermineRevision, e: print str(e) return print options.display(revision) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revision [revision]") parser.add_option("", "--location", action="store_const", const=paths.determine_path, dest="display", help="Show location instead of name", default=str) parser.add_option("--import", action="store_const", const=paths.determine_import_path, dest="display", help="Show location of import file") parser.add_option("--log", action="store_const", const=paths.determine_log_path, dest="display", help="Show location of log file") parser.add_option("--patch", action="store_const", dest="display", const=paths.determine_patch_path, help="Show location of patchfile") parser.add_option("--continuation", action="store_const", const=paths.determine_continuation_path, dest="display", help="Show location of continuation file") parser.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Expands aliases and prints the name of the specified revision. Instead of the name, several options can be used to print locations. If more than one is specified, the last one is used. """ help_tree_spec() return def require_version_exists(version, spec): if not version.exists(): raise cmdutil.CantDetermineVersion(spec, "The version %s does not exist." \ % version) class Revisions(BaseCommand): """ Print a revision name based on a revision specifier """ def __init__(self): self.description="Lists revisions" def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ (options, args) = self.get_parser().parse_args(cmdargs) if len(args) > 1: raise cmdutil.GetHelp try: self.tree = arch.tree_root() except arch.errors.TreeRootError: self.tree = None try: iter = self.get_iterator(options.type, args, options.reverse, options.modified) except cmdutil.CantDetermineRevision, e: raise CommandFailedWrapper(e) if options.skip is not None: iter = cmdutil.iter_skip(iter, int(options.skip)) for revision in iter: log = None if isinstance(revision, arch.Patchlog): log = revision revision=revision.revision print options.display(revision) if log is None and (options.summary or options.creator or options.date or options.merges): log = revision.patchlog if options.creator: print " %s" % log.creator if options.date: print " %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date) if options.summary: print " %s" % log.summary if options.merges: showed_title = False for revision in log.merged_patches: if not showed_title: print " Merged:" showed_title = True print " %s" % revision def get_iterator(self, type, args, reverse, modified): if len(args) > 0: spec = args[0] else: spec = None if modified is not None: iter = cmdutil.modified_iter(modified, self.tree) if reverse: return iter else: return cmdutil.iter_reverse(iter) elif type == "archive": if spec is None: if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) else: version = cmdutil.determine_version_arch(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) return version.iter_revisions(reverse) elif type == "cacherevs": if spec is None: if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) else: version = cmdutil.determine_version_arch(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) return cmdutil.iter_cacherevs(version, reverse) elif type == "library": if spec is None: if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) else: version = cmdutil.determine_version_arch(spec, self.tree) return version.iter_library_revisions(reverse) elif type == "logs": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \ self.tree), reverse) elif type == "missing" or type == "skip-present": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") skip = (type == "skip-present") version = cmdutil.determine_version_tree(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) return cmdutil.iter_missing(self.tree, version, reverse, skip_present=skip) elif type == "present": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) return cmdutil.iter_present(self.tree, version, reverse) elif type == "new-merges" or type == "direct-merges": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") version = cmdutil.determine_version_tree(spec, self.tree) cmdutil.ensure_archive_registered(version.archive) require_version_exists(version, spec) iter = cmdutil.iter_new_merges(self.tree, version, reverse) if type == "new-merges": return iter elif type == "direct-merges": return cmdutil.direct_merges(iter) elif type == "missing-from": if self.tree is None: raise cmdutil.CantDetermineRevision("", "Not in a project tree") revision = cmdutil.determine_revision_tree(self.tree, spec) libtree = cmdutil.find_or_make_local_revision(revision) return cmdutil.iter_missing(libtree, self.tree.tree_version, reverse) elif type == "partner-missing": return cmdutil.iter_partner_missing(self.tree, reverse) elif type == "ancestry": revision = cmdutil.determine_revision_tree(self.tree, spec) iter = cmdutil._iter_ancestry(self.tree, revision) if reverse: return iter else: return cmdutil.iter_reverse(iter) elif type == "dependencies" or type == "non-dependencies": nondeps = (type == "non-dependencies") revision = cmdutil.determine_revision_tree(self.tree, spec) anc_iter = cmdutil._iter_ancestry(self.tree, revision) iter_depends = cmdutil.iter_depends(anc_iter, nondeps) if reverse: return iter_depends else: return cmdutil.iter_reverse(iter_depends) elif type == "micro": return cmdutil.iter_micro(self.tree) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai revisions [revision]") select = cmdutil.OptionGroup(parser, "Selection options", "Control which revisions are listed. These options" " are mutually exclusive. If more than one is" " specified, the last is used.") select.add_option("", "--archive", action="store_const", const="archive", dest="type", default="archive", help="List all revisions in the archive") select.add_option("", "--cacherevs", action="store_const", const="cacherevs", dest="type", help="List all revisions stored in the archive as " "complete copies") select.add_option("", "--logs", action="store_const", const="logs", dest="type", help="List revisions that have a patchlog in the " "tree") select.add_option("", "--missing", action="store_const", const="missing", dest="type", help="List revisions from the specified version that" " have no patchlog in the tree") select.add_option("", "--skip-present", action="store_const", const="skip-present", dest="type", help="List revisions from the specified version that" " have no patchlogs at all in the tree") select.add_option("", "--present", action="store_const", const="present", dest="type", help="List revisions from the specified version that" " have no patchlog in the tree, but can't be merged") select.add_option("", "--missing-from", action="store_const", const="missing-from", dest="type", help="List revisions from the specified revision " "that have no patchlog for the tree version") select.add_option("", "--partner-missing", action="store_const", const="partner-missing", dest="type", help="List revisions in partner versions that are" " missing") select.add_option("", "--new-merges", action="store_const", const="new-merges", dest="type", help="List revisions that have had patchlogs added" " to the tree since the last commit") select.add_option("", "--direct-merges", action="store_const", const="direct-merges", dest="type", help="List revisions that have been directly added" " to tree since the last commit ") select.add_option("", "--library", action="store_const", const="library", dest="type", help="List revisions in the revision library") select.add_option("", "--ancestry", action="store_const", const="ancestry", dest="type", help="List revisions that are ancestors of the " "current tree version") select.add_option("", "--dependencies", action="store_const", const="dependencies", dest="type", help="List revisions that the given revision " "depends on") select.add_option("", "--non-dependencies", action="store_const", const="non-dependencies", dest="type", help="List revisions that the given revision " "does not depend on") select.add_option("--micro", action="store_const", const="micro", dest="type", help="List partner revisions aimed for this " "micro-branch") select.add_option("", "--modified", dest="modified", help="List tree ancestor revisions that modified a " "given file", metavar="FILE[:LINE]") parser.add_option("", "--skip", dest="skip", help="Skip revisions. Positive numbers skip from " "beginning, negative skip from end.", metavar="NUMBER") parser.add_option_group(select) format = cmdutil.OptionGroup(parser, "Revision format options", "These control the appearance of listed revisions") format.add_option("", "--location", action="store_const", const=paths.determine_path, dest="display", help="Show location instead of name", default=str) format.add_option("--import", action="store_const", const=paths.determine_import_path, dest="display", help="Show location of import file") format.add_option("--log", action="store_const", const=paths.determine_log_path, dest="display", help="Show location of log file") format.add_option("--patch", action="store_const", dest="display", const=paths.determine_patch_path, help="Show location of patchfile") format.add_option("--continuation", action="store_const", const=paths.determine_continuation_path, dest="display", help="Show location of continuation file") format.add_option("--cacherev", action="store_const", const=paths.determine_cacherev_path, dest="display", help="Show location of cacherev file") parser.add_option_group(format) display = cmdutil.OptionGroup(parser, "Display format options", "These control the display of data") display.add_option("-r", "--reverse", action="store_true", dest="reverse", help="Sort from newest to oldest") display.add_option("-s", "--summary", action="store_true", dest="summary", help="Show patchlog summary") display.add_option("-D", "--date", action="store_true", dest="date", help="Show patchlog date") display.add_option("-c", "--creator", action="store_true", dest="creator", help="Show the id that committed the" " revision") display.add_option("-m", "--merges", action="store_true", dest="merges", help="Show the revisions that were" " merged") parser.add_option_group(display) return parser def help(self, parser=None): """Attempt to explain the revisions command :param parser: If supplied, used to determine options """ if parser==None: parser=self.get_parser() parser.print_help() print """List revisions. """ help_tree_spec() class Get(BaseCommand): """ Retrieve a revision from the archive """ def __init__(self): self.description="Retrieve a revision from the archive" self.parser=self.get_parser() def get_completer(self, arg, index): if index > 0: return None try: tree = arch.tree_root() except: tree = None return cmdutil.iter_revision_completions(arg, tree) def do_command(self, cmdargs): """ Master function that perfoms the "get" command. """ (options, args) = self.parser.parse_args(cmdargs) if len(args) < 1: return self.help() try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None arch_loc = None try: revision, arch_loc = paths.full_path_decode(args[0]) except Exception, e: revision = cmdutil.determine_revision_arch(tree, args[0], check_existence=False, allow_package=True) if len(args) > 1: directory = args[1] else: directory = str(revision.nonarch) if os.path.exists(directory): raise DirectoryExists(directory) cmdutil.ensure_archive_registered(revision.archive, arch_loc) try: cmdutil.ensure_revision_exists(revision) except cmdutil.NoSuchRevision, e: raise CommandFailedWrapper(e) link = cmdutil.prompt ("get link") for line in cmdutil.iter_get(revision, directory, link, options.no_pristine, options.no_greedy_add): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "get" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai get revision [dir]") parser.add_option("--no-pristine", action="store_true", dest="no_pristine", help="Do not make pristine copy for reference") parser.add_option("--no-greedy-add", action="store_true", dest="no_greedy_add", help="Never add to greedy libraries") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Expands aliases and constructs a project tree for a revision. If the optional "dir" argument is provided, the project tree will be stored in this directory. """ help_tree_spec() return class PromptCmd(cmd.Cmd): def __init__(self): cmd.Cmd.__init__(self) self.prompt = "Fai> " try: self.tree = arch.tree_root() except: self.tree = None self.set_title() self.set_prompt() self.fake_aba = abacmds.AbaCmds() self.identchars += '-' self.history_file = os.path.expanduser("~/.fai-history") readline.set_completer_delims(string.whitespace) if os.access(self.history_file, os.R_OK) and \ os.path.isfile(self.history_file): readline.read_history_file(self.history_file) def write_history(self): readline.write_history_file(self.history_file) def do_quit(self, args): self.write_history() sys.exit(0) def do_exit(self, args): self.do_quit(args) def do_EOF(self, args): print self.do_quit(args) def postcmd(self, line, bar): self.set_title() self.set_prompt() def set_prompt(self): if self.tree is not None: try: version = " "+self.tree.tree_version.nonarch except: version = "" else: version = "" self.prompt = "Fai%s> " % version def set_title(self, command=None): try: version = self.tree.tree_version.nonarch except: version = "[no version]" if command is None: command = "" sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version))) def do_cd(self, line): if line == "": line = "~" try: os.chdir(os.path.expanduser(line)) except Exception, e: print e try: self.tree = arch.tree_root() except: self.tree = None def do_help(self, line): Help()(line) def default(self, line): args = line.split() if find_command(args[0]): try: find_command(args[0]).do_command(args[1:]) except cmdutil.BadCommandOption, e: print e except cmdutil.GetHelp, e: find_command(args[0]).help() except CommandFailed, e: print e except arch.errors.ArchiveNotRegistered, e: print e except KeyboardInterrupt, e: print "Interrupted" except arch.util.ExecProblem, e: print e.proc.error.rstrip('\n') except cmdutil.CantDetermineVersion, e: print e except cmdutil.CantDetermineRevision, e: print e except Exception, e: print "Unhandled error:\n%s" % cmdutil.exception_str(e) elif suggestions.has_key(args[0]): print suggestions[args[0]] elif self.fake_aba.is_command(args[0]): tree = None try: tree = arch.tree_root() except arch.errors.TreeRootError: pass cmd = self.fake_aba.is_command(args[0]) try: cmd.run(cmdutil.expand_prefix_alias(args[1:], tree)) except KeyboardInterrupt, e: print "Interrupted" elif options.tla_fallthrough and args[0] != "rm" and \ cmdutil.is_tla_command(args[0]): try: tree = None try: tree = arch.tree_root() except arch.errors.TreeRootError: pass args = cmdutil.expand_prefix_alias(args, tree) arch.util.exec_safe('tla', args, stderr=sys.stderr, expected=(0, 1)) except arch.util.ExecProblem, e: pass except KeyboardInterrupt, e: print "Interrupted" else: try: try: tree = arch.tree_root() except arch.errors.TreeRootError: tree = None args=line.split() os.system(" ".join(cmdutil.expand_prefix_alias(args, tree))) except KeyboardInterrupt, e: print "Interrupted" def completenames(self, text, line, begidx, endidx): completions = [] iter = iter_command_names(self.fake_aba) try: if len(line) > 0: arg = line.split()[-1] else: arg = "" iter = iter_munged_completions(iter, arg, text) except Exception, e: print e return list(iter) def completedefault(self, text, line, begidx, endidx): """Perform completion for native commands. :param text: The text to complete :type text: str :param line: The entire line to complete :type line: str :param begidx: The start of the text in the line :type begidx: int :param endidx: The end of the text in the line :type endidx: int """ try: (cmd, args, foo) = self.parseline(line) command_obj=find_command(cmd) if command_obj is not None: return command_obj.complete(args.split(), text) elif not self.fake_aba.is_command(cmd) and \ cmdutil.is_tla_command(cmd): iter = cmdutil.iter_supported_switches(cmd) if len(args) > 0: arg = args.split()[-1] else: arg = "" if arg.startswith("-"): return list(iter_munged_completions(iter, arg, text)) else: return list(iter_munged_completions( iter_file_completions(arg), arg, text)) elif cmd == "cd": if len(args) > 0: arg = args.split()[-1] else: arg = "" iter = iter_dir_completions(arg) iter = iter_munged_completions(iter, arg, text) return list(iter) elif len(args)>0: arg = args.split()[-1] return list(iter_munged_completions(iter_file_completions(arg), arg, text)) else: return self.completenames(text, line, begidx, endidx) except Exception, e: print e def iter_command_names(fake_aba): for entry in cmdutil.iter_combine([commands.iterkeys(), fake_aba.get_commands(), cmdutil.iter_tla_commands(False)]): if not suggestions.has_key(str(entry)): yield entry def iter_file_completions(arg, only_dirs = False): """Generate an iterator that iterates through filename completions. :param arg: The filename fragment to match :type arg: str :param only_dirs: If true, match only directories :type only_dirs: bool """ cwd = os.getcwd() if cwd != "/": extras = [".", ".."] else: extras = [] (dir, file) = os.path.split(arg) if dir != "": listingdir = os.path.expanduser(dir) else: listingdir = cwd for file in cmdutil.iter_combine([os.listdir(listingdir), extras]): if dir != "": userfile = dir+'/'+file else: userfile = file if userfile.startswith(arg): if os.path.isdir(listingdir+'/'+file): userfile+='/' yield userfile elif not only_dirs: yield userfile def iter_munged_completions(iter, arg, text): for completion in iter: completion = str(completion) if completion.startswith(arg): yield completion[len(arg)-len(text):] def iter_source_file_completions(tree, arg): treepath = cmdutil.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: dirs = None for file in tree.iter_inventory(dirs, source=True, both=True): file = file_completion_match(file, treepath, arg) if file is not None: yield file def iter_untagged(tree, dirs): for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, categories=arch_core.non_root, control_files=True): yield file.name def iter_untagged_completions(tree, arg): """Generate an iterator for all visible untagged files that match arg. :param tree: The tree to look for untagged files in :type tree: `arch.WorkingTree` :param arg: The argument to match :type arg: str :return: An iterator of all matching untagged files :rtype: iterator of str """ treepath = cmdutil.tree_cwd(tree) if len(treepath) > 0: dirs = [treepath] else: dirs = None for file in iter_untagged(tree, dirs): file = file_completion_match(file, treepath, arg) if file is not None: yield file def file_completion_match(file, treepath, arg): """Determines whether a file within an arch tree matches the argument. :param file: The rooted filename :type file: str :param treepath: The path to the cwd within the tree :type treepath: str :param arg: The prefix to match :return: The completion name, or None if not a match :rtype: str """ if not file.startswith(treepath): return None if treepath != "": file = file[len(treepath)+1:] if not file.startswith(arg): return None if os.path.isdir(file): file += '/' return file def iter_modified_file_completions(tree, arg): """Returns a list of modified files that match the specified prefix. :param tree: The current tree :type tree: `arch.WorkingTree` :param arg: The prefix to match :type arg: str """ treepath = cmdutil.tree_cwd(tree) tmpdir = cmdutil.tmpdir() changeset = tmpdir+"/changeset" completions = [] revision = cmdutil.determine_revision_tree(tree) for line in arch.iter_delta(revision, tree, changeset): if isinstance(line, arch.FileModification): file = file_completion_match(line.name[1:], treepath, arg) if file is not None: completions.append(file) shutil.rmtree(tmpdir) return completions def iter_dir_completions(arg): """Generate an iterator that iterates through directory name completions. :param arg: The directory name fragment to match :type arg: str """ return iter_file_completions(arg, True) class Shell(BaseCommand): def __init__(self): self.description = "Runs Fai as a shell" def do_command(self, cmdargs): if len(cmdargs)!=0: raise cmdutil.GetHelp prompt = PromptCmd() try: prompt.cmdloop() finally: prompt.write_history() class AddID(BaseCommand): """ Adds an inventory id for the given file """ def __init__(self): self.description="Add an inventory id for a given file" def get_completer(self, arg, index): tree = arch.tree_root() return iter_untagged_completions(tree, arg) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) tree = arch.tree_root() if (len(args) == 0) == (options.untagged == False): raise cmdutil.GetHelp #if options.id and len(args) != 1: # print "If --id is specified, only one file can be named." # return method = tree.tagging_method if options.id_type == "tagline": if method != "tagline": if not cmdutil.prompt("Tagline in other tree"): if method == "explicit": options.id_type == explicit else: print "add-id not supported for \"%s\" tagging method"\ % method return elif options.id_type == "explicit": if method != "tagline" and method != explicit: if not prompt("Explicit in other tree"): print "add-id not supported for \"%s\" tagging method" % \ method return if options.id_type == "auto": if method != "tagline" and method != "explicit": print "add-id not supported for \"%s\" tagging method" % method return else: options.id_type = method if options.untagged: args = None self.add_ids(tree, options.id_type, args) def add_ids(self, tree, id_type, files=()): """Add inventory ids to files. :param tree: the tree the files are in :type tree: `arch.WorkingTree` :param id_type: the type of id to add: "explicit" or "tagline" :type id_type: str :param files: The list of files to add. If None do all untagged. :type files: tuple of str """ untagged = (files is None) if untagged: files = list(iter_untagged(tree, None)) previous_files = [] while len(files) > 0: previous_files.extend(files) if id_type == "explicit": cmdutil.add_id(files) elif id_type == "tagline": for file in files: try: cmdutil.add_tagline_or_explicit_id(file) except cmdutil.AlreadyTagged: print "\"%s\" already has a tagline." % file except cmdutil.NoCommentSyntax: pass #do inventory after tagging until no untagged files are encountered if untagged: files = [] for file in iter_untagged(tree, None): if not file in previous_files: files.append(file) else: break def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...") # ddaa suggests removing this to promote GUIDs. Let's see who squalks. # parser.add_option("-i", "--id", dest="id", # help="Specify id for a single file", default=None) parser.add_option("--tltl", action="store_true", dest="lord_style", help="Use Tom Lord's style of id.") parser.add_option("--explicit", action="store_const", const="explicit", dest="id_type", help="Use an explicit id", default="auto") parser.add_option("--tagline", action="store_const", const="tagline", dest="id_type", help="Use a tagline id") parser.add_option("--untagged", action="store_true", dest="untagged", default=False, help="tag all untagged files") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Adds an inventory to the specified file(s) and directories. If --untagged is specified, adds inventory to all untagged files and directories. """ return class Merge(BaseCommand): """ Merges changes from other versions into the current tree """ def __init__(self): self.description="Merges changes from other versions" try: self.tree = arch.tree_root() except: self.tree = None def get_completer(self, arg, index): if self.tree is None: raise arch.errors.TreeRootError completions = list(ancillary.iter_partners(self.tree, self.tree.tree_version)) if len(completions) == 0: completions = list(self.tree.iter_log_versions()) aliases = [] try: for completion in completions: alias = ancillary.compact_alias(str(completion), self.tree) if alias: aliases.extend(alias) for completion in completions: if completion.archive == self.tree.tree_version.archive: aliases.append(completion.nonarch) except Exception, e: print e completions.extend(aliases) return completions def do_command(self, cmdargs): """ Master function that perfoms the "merge" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if options.diff3: action="star-merge" else: action = options.action if self.tree is None: raise arch.errors.TreeRootError(os.getcwd()) if cmdutil.has_changed(self.tree.tree_version): raise UncommittedChanges(self.tree) if len(args) > 0: revisions = [] for arg in args: revisions.append(cmdutil.determine_revision_arch(self.tree, arg)) source = "from commandline" else: revisions = ancillary.iter_partner_revisions(self.tree, self.tree.tree_version) source = "from partner version" revisions = misc.rewind_iterator(revisions) try: revisions.next() revisions.rewind() except StopIteration, e: revision = cmdutil.tag_cur(self.tree) if revision is None: raise CantDetermineRevision("", "No version specified, no " "partner-versions, and no tag" " source") revisions = [revision] source = "from tag source" for revision in revisions: cmdutil.ensure_archive_registered(revision.archive) cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % (revision, source))) if action=="native-merge" or action=="update": if self.native_merge(revision, action) == 0: continue elif action=="star-merge": try: self.star_merge(revision, options.diff3) except errors.MergeProblem, e: break if cmdutil.has_changed(self.tree.tree_version): break def star_merge(self, revision, diff3): """Perform a star-merge on the current tree. :param revision: The revision to use for the merge :type revision: `arch.Revision` :param diff3: If true, do a diff3 merge :type diff3: bool """ try: for line in self.tree.iter_star_merge(revision, diff3=diff3): cmdutil.colorize(line) except arch.util.ExecProblem, e: if e.proc.status is not None and e.proc.status == 1: if e.proc.error: print e.proc.error raise MergeProblem else: raise def native_merge(self, other_revision, action): """Perform a native-merge on the current tree. :param other_revision: The revision to use for the merge :type other_revision: `arch.Revision` :return: 0 if the merge was skipped, 1 if it was applied """ other_tree = cmdutil.find_or_make_local_revision(other_revision) try: if action == "native-merge": ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, other_revision) elif action == "update": ancestor = cmdutil.tree_latest(self.tree, other_revision.version) except CantDetermineRevision, e: raise CommandFailedWrapper(e) cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor)) if (ancestor == other_revision): cmdutil.colorize(arch.Chatter("* Skipping redundant merge" % ancestor)) return 0 delta = cmdutil.apply_delta(ancestor, other_tree, self.tree) for line in cmdutil.iter_apply_delta_filter(delta): cmdutil.colorize(line) return 1 def get_parser(self): """ Returns the options parser to use for the "merge" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai merge [VERSION]") parser.add_option("-s", "--star-merge", action="store_const", dest="action", help="Use star-merge", const="star-merge", default="native-merge") parser.add_option("--update", action="store_const", dest="action", help="Use update picker", const="update") parser.add_option("--diff3", action="store_true", dest="diff3", help="Use diff3 for merge (implies star-merge)") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Performs a merge operation using the specified version. """ return class ELog(BaseCommand): """ Produces a raw patchlog and invokes the user's editor """ def __init__(self): self.description="Edit a patchlog to commit" try: self.tree = arch.tree_root() except: self.tree = None def do_command(self, cmdargs): """ Master function that perfoms the "elog" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if self.tree is None: raise arch.errors.TreeRootError edit_log(self.tree) def get_parser(self): """ Returns the options parser to use for the "merge" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai elog") return parser def help(self, parser=None): """ Invokes $EDITOR to produce a log for committing. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Invokes $EDITOR to produce a log for committing. """ return def edit_log(tree): """Makes and edits the log for a tree. Does all kinds of fancy things like log templates and merge summaries and log-for-merge :param tree: The tree to edit the log for :type tree: `arch.WorkingTree` """ #ensure we have an editor before preparing the log cmdutil.find_editor() log = tree.log_message(create=False) log_is_new = False if log is None or cmdutil.prompt("Overwrite log"): if log is not None: os.remove(log.name) log = tree.log_message(create=True) log_is_new = True tmplog = log.name template = tree+"/{arch}/=log-template" if not os.path.exists(template): template = os.path.expanduser("~/.arch-params/=log-template") if not os.path.exists(template): template = None if template: shutil.copyfile(template, tmplog) new_merges = list(cmdutil.iter_new_merges(tree, tree.tree_version)) log["Summary"] = merge_summary(new_merges, tree.tree_version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): mergestuff = cmdutil.log_for_merge(tree) log.description += mergestuff log.save() try: cmdutil.invoke_editor(log.name) except: if log_is_new: os.remove(log.name) raise def merge_summary(new_merges, tree_version): if len(new_merges) == 0: return "" if len(new_merges) == 1: summary = new_merges[0].summary else: summary = "Merge" credits = [] for merge in new_merges: if arch.my_id() != merge.creator: name = re.sub("<.*>", "", merge.creator).rstrip(" "); if not name in credits: credits.append(name) else: version = merge.revision.version if version.archive == tree_version.archive: if not version.nonarch in credits: credits.append(version.nonarch) elif not str(version) in credits: credits.append(str(version)) return ("%s (%s)") % (summary, ", ".join(credits)) class MirrorArchive(BaseCommand): """ Updates a mirror from an archive """ def __init__(self): self.description="Update a mirror from an archive" def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) if len(args) > 1: raise GetHelp try: tree = arch.tree_root() except: tree = None if len(args) == 0: if tree is not None: name = tree.tree_version() else: name = cmdutil.expand_alias(args[0], tree) name = arch.NameParser(name) to_arch = name.get_archive() from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch)) limit = name.get_nonarch() iter = arch_core.mirror_archive(from_arch,to_arch, limit) for line in arch.chatter_classifier(iter): cmdutil.colorize(line) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Updates a mirror from an archive. If a branch, package, or version is supplied, only changes under it are mirrored. """ return def help_tree_spec(): print """Specifying revisions (default: tree) Revisions may be specified by alias, revision, version or patchlevel. Revisions or versions may be fully qualified. Unqualified revisions, versions, or patchlevels use the archive of the current project tree. Versions will use the latest patchlevel in the tree. Patchlevels will use the current tree- version. Use "alias" to list available (user and automatic) aliases.""" def help_aliases(tree): print """Auto-generated aliases acur : The latest revision in the archive of the tree-version. You can specfy a different version like so: acur:foo--bar--0 (aliases can be used) tcur : (tree current) The latest revision in the tree of the tree-version. You can specify a different version like so: tcur:foo--bar--0 (aliases can be used). tprev : (tree previous) The previous revision in the tree of the tree-version. To specify an older revision, use a number, e.g. "tprev:4" tanc : (tree ancestor) The ancestor revision of the tree To specify an older revision, use a number, e.g. "tanc:4" tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6") tmod : (tree modified) The latest revision to modify a given file (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16") ttag : (tree tag) The revision that was tagged into the current tree revision, according to the tree. tagcur: (tag current) The latest revision of the version that the current tree was tagged from. mergeanc : The common ancestor of the current tree and the specified revision. Defaults to the first partner-version's latest revision or to tagcur. """ print "User aliases" for parts in ancillary.iter_all_alias(tree): print parts[0].rjust(10)+" : "+parts[1] class Inventory(BaseCommand): """List the status of files in the tree""" def __init__(self): self.description=self.__doc__ def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) tree = arch.tree_root() categories = [] if (options.source): categories.append(arch_core.SourceFile) if (options.precious): categories.append(arch_core.PreciousFile) if (options.backup): categories.append(arch_core.BackupFile) if (options.junk): categories.append(arch_core.JunkFile) if len(categories) == 1: show_leading = False else: show_leading = True if len(categories) == 0: categories = None if options.untagged: categories = arch_core.non_root show_leading = False tagged = False else: tagged = None for file in arch_core.iter_inventory_filter(tree, None, control_files=options.control_files, categories = categories, tagged=tagged): print arch_core.file_line(file, category = show_leading, untagged = show_leading, id = options.ids) def get_parser(self): """ Returns the options parser to use for the "revision" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai inventory [options]") parser.add_option("--ids", action="store_true", dest="ids", help="Show file ids") parser.add_option("--control", action="store_true", dest="control_files", help="include control files") parser.add_option("--source", action="store_true", dest="source", help="List source files") parser.add_option("--backup", action="store_true", dest="backup", help="List backup files") parser.add_option("--precious", action="store_true", dest="precious", help="List precious files") parser.add_option("--junk", action="store_true", dest="junk", help="List junk files") parser.add_option("--unrecognized", action="store_true", dest="unrecognized", help="List unrecognized files") parser.add_option("--untagged", action="store_true", dest="untagged", help="List only untagged files") return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Lists the status of files in the archive: S source P precious B backup J junk U unrecognized T tree root ? untagged-source Leading letter are not displayed if only one kind of file is shown """ return class Alias(BaseCommand): """List or adjust aliases""" def __init__(self): self.description=self.__doc__ def get_completer(self, arg, index): if index > 2: return () try: self.tree = arch.tree_root() except: self.tree = None if index == 0: return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)] elif index == 1: return cmdutil.iter_revision_completions(arg, self.tree) def do_command(self, cmdargs): """ Master function that perfoms the "revision" command. """ parser=self.get_parser() (options, args) = parser.parse_args(cmdargs) try: self.tree = arch.tree_root() except: self.tree = None try: options.action(args, options) except cmdutil.ForbiddenAliasSyntax, e: raise CommandFailedWrapper(e) def arg_dispatch(self, args, options): """Add, modify, or list aliases, depending on number of arguments :param args: The list of commandline arguments :type args: list of str :param options: The commandline options """ if len(args) == 0: help_aliases(self.tree) return elif len(args) == 1: self.print_alias(args[0]) elif (len(args)) == 2: self.add(args[0], args[1], options) else: raise cmdutil.GetHelp def print_alias(self, alias): answer = None for pair in ancillary.iter_all_alias(self.tree): if pair[0] == alias: answer = pair[1] if answer is not None: print answer else: print "The alias %s is not assigned." % alias def add(self, alias, expansion, options): """Add or modify aliases :param alias: The alias name to create/modify :type alias: str :param expansion: The expansion to assign to the alias name :type expansion: str :param options: The commandline options """ newlist = "" written = False new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, self.tree)) ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion]) for pair in self.get_iterator(options): if pair[0] != alias: newlist+="%s=%s\n" % (pair[0], pair[1]) elif not written: newlist+=new_line written = True if not written: newlist+=new_line self.write_aliases(newlist, options) def delete(self, args, options): """Delete the specified alias :param args: The list of arguments :type args: list of str :param options: The commandline options """ deleted = False if len(args) != 1: raise cmdutil.GetHelp newlist = "" for pair in self.get_iterator(options): if pair[0] != args[0]: newlist+="%s=%s\n" % (pair[0], pair[1]) else: deleted = True if not deleted: raise errors.NoSuchAlias(args[0]) self.write_aliases(newlist, options) def get_alias_file(self, options): """Return the name of the alias file to use :param options: The commandline options """ if options.tree: if self.tree is None: self.tree == arch.tree_root() return str(self.tree)+"/{arch}/+aliases" else: return "~/.aba/aliases" def get_iterator(self, options): """Return the alias iterator to use :param options: The commandline options """ return ancillary.iter_alias(self.get_alias_file(options)) def write_aliases(self, newlist, options): """Safely rewrite the alias file :param newlist: The new list of aliases :type newlist: str :param options: The commandline options """ filename = os.path.expanduser(self.get_alias_file(options)) file = cmdutil.NewFileVersion(filename) file.write(newlist) file.commit() def get_parser(self): """ Returns the options parser to use for the "alias" command. :rtype: cmdutil.CmdOptionParser """ parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]") parser.add_option("-d", "--delete", action="store_const", dest="action", const=self.delete, default=self.arg_dispatch, help="Delete an alias") parser.add_option("--tree", action="store_true", dest="tree", help="Create a per-tree alias", default=False) return parser def help(self, parser=None): """ Prints a help message. :param parser: If supplied, the parser to use for generating help. If \ not supplied, it is retrieved. :type parser: cmdutil.CmdOptionParser """ if parser==None: parser=self.get_parser() parser.print_help() print """ Lists current aliases or modifies the list of aliases. If no arguments are supplied, aliases will be listed. If two arguments are supplied, the specified alias will be created or modified. If -d or --delete is supplied, the specified alias will be deleted. You can create aliases that refer to any fully-qualified part of the Arch namespace, e.g. archive, archive/category, archive/category--branch, archive/category--branch--version (my favourite) archive/category--branch--version--patchlevel Aliases can be used automatically by native commands. To use them with external or tla commands, prefix them with ^ (you can do this with native commands, too). """ class RequestMerge(BaseCommand): """Submit a merge request to Bug Goo""" def __init__(self): self.description=self.__doc__ def do_command(self, cmdargs): """Submit a merge request :param cmdargs: The commandline arguments :type cmdargs: list of str """ cmdutil.find_editor() parser = self.get_parser() (options, args) = parser.parse_args(cmdargs) try: self.tree=arch.tree_root() except: self.tree=None base, revisions = self.revision_specs(args) message = self.make_headers(base, revisions) message += self.make_summary(revisions) path = self.edit_message(message) message = self.tidy_message(path) if cmdutil.prompt("Send merge"): self.send_message(message) print "Merge request sent" def make_headers(self, base, revisions): """Produce email and Bug Goo header strings :param base: The base revision to apply merges to :type base: `arch.Revision` :param revisions: The revisions to replay into the base :type revisions: list of `arch.Patchlog` :return: The headers :rtype: str """ headers = "To: gnu-arch-users@gnu.org\n" headers += "From: %s\n" % options.fromaddr if len(revisions) == 1: headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary else: headers += "Subject: [MERGE REQUEST]\n" headers += "\n" headers += "Base-Revision: %s\n" % base for revision in revisions: headers += "Revision: %s\n" % revision.revision headers += "Bug: \n\n" return headers def make_summary(self, logs): """Generate a summary of merges :param logs: the patchlogs that were directly added by the merges :type logs: list of `arch.Patchlog` :return: the summary :rtype: str """ summary = "" for log in logs: summary+=str(log.revision)+"\n" summary+=log.summary+"\n" if log.description.strip(): summary+=log.description.strip('\n')+"\n\n" return summary def revision_specs(self, args): """Determine the base and merge revisions from tree and arguments. :param args: The parsed arguments :type args: list of str :return: The base revision and merge revisions :rtype: `arch.Revision`, list of `arch.Patchlog` """ if len(args) > 0: target_revision = cmdutil.determine_revision_arch(self.tree, args[0]) else: target_revision = cmdutil.tree_latest(self.tree) if len(args) > 1: merges = [ arch.Patchlog(cmdutil.determine_revision_arch( self.tree, f)) for f in args[1:] ] else: if self.tree is None: raise CantDetermineRevision("", "Not in a project tree") merge_iter = cmdutil.iter_new_merges(self.tree, target_revision.version, False) merges = [f for f in cmdutil.direct_merges(merge_iter)] return (target_revision, merges) def edit_message(self, message): """Edit an email message in the user's standard editor :param message: The message to edit :type message: str :return: the path of the edited message :rtype: str """ if self.tree is None: path = os.get_cwd() else: path = self.tree path += "/,merge-request" file = open(path, 'w') file.write(message) file.flush() cmdutil.invoke_editor(path) return path def tidy_message(self, path): """Validate and clean up message. :param path: The path to the message to clean up :type path: str :return: The parsed message :rtype: `email.Message` """ mail = email.message_from_file(open(path)) if mail["Subject"].strip() == "[MERGE REQUEST]": raise BlandSubject request = email.message_from_string(mail.get_payload()) if request.has_key("Bug"): if request["Bug"].strip()=="": del request["Bug"] mail.set_payload(request.as_string()) return mail def send_message(self, message): """Send a message, using its headers to address it. :param message: The message to send :type message: `email.Message`""" server = smtplib.SMTP() server.sendmail(message['From'], message['To'], message.as_string()) server.quit() def help(self, parser=None): """Print a usage message :param parser: The options parser to use :type parser: `cmdutil.CmdOptionParser` """ if parser is None: parser = self.get_parser() parser.print_help() print """ Sends a merge request formatted for Bug Goo. Intended use: get the tree you'd like to merge into. Apply the merges you want. Invoke request-merge. The merge request will open in your $EDITOR. When no TARGET is specified, it uses the current tree revision. When no MERGE is specified, it uses the direct merges (as in "revisions --direct-merges"). But you can specify just the TARGET, or all the MERGE revisions. """ def get_parser(self): """Produce a commandline parser for this command. :rtype: `cmdutil.CmdOptionParser` """ parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]") return parser commands = { 'changes' : Changes, 'help' : Help, 'update': Update, 'apply-changes':ApplyChanges, 'cat-log': CatLog, 'commit': Commit, 'revision': Revision, 'revisions': Revisions, 'get': Get, 'revert': Revert, 'shell': Shell, 'add-id': AddID, 'merge': Merge, 'elog': ELog, 'mirror-archive': MirrorArchive, 'ninventory': Inventory, 'alias' : Alias, 'request-merge': RequestMerge, } suggestions = { 'apply-delta' : "Try \"apply-changes\".", 'delta' : "To compare two revisions, use \"changes\".", 'diff-rev' : "To compare two revisions, use \"changes\".", 'undo' : "To undo local changes, use \"revert\".", 'undelete' : "To undo only deletions, use \"revert --deletions\"", 'missing-from' : "Try \"revisions --missing-from\".", 'missing' : "Try \"revisions --missing\".", 'missing-merge' : "Try \"revisions --partner-missing\".", 'new-merges' : "Try \"revisions --new-merges\".", 'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')", 'logs' : "Try \"revisions --logs\"", 'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")", 'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")", 'change-version' : "Try \"update REVISION\"", 'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")", 'rev-depends' : "Use revisions --dependencies", 'auto-get' : "Plain get will do archive lookups", 'tagline' : "Use add-id. It uses taglines in tagline trees", 'emlog' : "Use elog. It automatically adds log-for-merge text, if any", 'library-revisions' : "Use revisions --library", 'file-revert' : "Use revert FILE" } # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7 patchkit-0.1.8/test_patches_data/orig-2000064400000000000000000000441021046102023000161430ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.1.8/test_patches_data/orig-3000064400000000000000000000441611046102023000161510ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 for line in orig_lines: yield line import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.1.8/test_patches_data/orig-4000064400000000000000000000441021046102023000161450ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.1.8/test_patches_data/orig-5000064400000000000000000000441021046102023000161460ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.1.8/test_patches_data/orig-6000064400000000000000000000441021046102023000161470ustar 00000000000000# Copyright (C) 2004, 2005 Aaron Bentley # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA class PatchSyntax(Exception): def __init__(self, msg): Exception.__init__(self, msg) class MalformedPatchHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedHunkHeader(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) PatchSyntax.__init__(self, msg) class MalformedLine(PatchSyntax): def __init__(self, desc, line): self.desc = desc self.line = line msg = "Malformed line. %s\n%s" % (self.desc, self.line) PatchSyntax.__init__(self, msg) def get_patch_names(iter_lines): try: line = iter_lines.next() if not line.startswith("--- "): raise MalformedPatchHeader("No orig name", line) else: orig_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No orig line", "") try: line = iter_lines.next() if not line.startswith("+++ "): raise PatchSyntax("No mod name") else: mod_name = line[4:].rstrip("\n") except StopIteration: raise MalformedPatchHeader("No mod line", "") return (orig_name, mod_name) def parse_range(textrange): """Parse a patch range, handling the "1" special-case :param textrange: The text to parse :type textrange: str :return: the position and range, as a tuple :rtype: (int, int) """ tmp = textrange.split(',') if len(tmp) == 1: pos = tmp[0] range = "1" else: (pos, range) = tmp pos = int(pos) range = int(range) return (pos, range) def hunk_from_header(line): if not line.startswith("@@") or not line.endswith("@@\n") \ or not len(line) > 4: raise MalformedHunkHeader("Does not start and end with @@.", line) try: (orig, mod) = line[3:-4].split(" ") except Exception, e: raise MalformedHunkHeader(str(e), line) if not orig.startswith('-') or not mod.startswith('+'): raise MalformedHunkHeader("Positions don't start with + or -.", line) try: (orig_pos, orig_range) = parse_range(orig[1:]) (mod_pos, mod_range) = parse_range(mod[1:]) except Exception, e: raise MalformedHunkHeader(str(e), line) if mod_range < 0 or orig_range < 0: raise MalformedHunkHeader("Hunk range is negative", line) return Hunk(orig_pos, orig_range, mod_pos, mod_range) class HunkLine: def __init__(self, contents): self.contents = contents def get_str(self, leadchar): if self.contents == "\n" and leadchar == " " and False: return "\n" if not self.contents.endswith('\n'): terminator = '\n' + NO_NL else: terminator = '' return leadchar + self.contents + terminator class ContextLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str(" ") class InsertLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("+") class RemoveLine(HunkLine): def __init__(self, contents): HunkLine.__init__(self, contents) def __str__(self): return self.get_str("-") NO_NL = '\\ No newline at end of file\n' __pychecker__="no-returnvalues" def parse_line(line): if line.startswith("\n"): return ContextLine(line) elif line.startswith(" "): return ContextLine(line[1:]) elif line.startswith("+"): return InsertLine(line[1:]) elif line.startswith("-"): return RemoveLine(line[1:]) elif line == NO_NL: return NO_NL else: raise MalformedLine("Unknown line type", line) __pychecker__="" class Hunk: def __init__(self, orig_pos, orig_range, mod_pos, mod_range): self.orig_pos = orig_pos self.orig_range = orig_range self.mod_pos = mod_pos self.mod_range = mod_range self.lines = [] def get_header(self): return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, self.orig_range), self.range_str(self.mod_pos, self.mod_range)) def range_str(self, pos, range): """Return a file range, special-casing for 1-line files. :param pos: The position in the file :type pos: int :range: The range in the file :type range: int :return: a string in the format 1,4 except when range == pos == 1 """ if range == 1: return "%i" % pos else: return "%i,%i" % (pos, range) def __str__(self): lines = [self.get_header()] for line in self.lines: lines.append(str(line)) return "".join(lines) def shift_to_mod(self, pos): if pos < self.orig_pos-1: return 0 elif pos > self.orig_pos+self.orig_range: return self.mod_range - self.orig_range else: return self.shift_to_mod_lines(pos) def shift_to_mod_lines(self, pos): assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) position = self.orig_pos-1 shift = 0 for line in self.lines: if isinstance(line, InsertLine): shift += 1 elif isinstance(line, RemoveLine): if position == pos: return None shift -= 1 position += 1 elif isinstance(line, ContextLine): position += 1 if position > pos: break return shift def iter_hunks(iter_lines): hunk = None for line in iter_lines: if line == "\n": if hunk is not None: yield hunk hunk = None continue if hunk is not None: yield hunk hunk = hunk_from_header(line) orig_size = 0 mod_size = 0 while orig_size < hunk.orig_range or mod_size < hunk.mod_range: hunk_line = parse_line(iter_lines.next()) hunk.lines.append(hunk_line) if isinstance(hunk_line, (RemoveLine, ContextLine)): orig_size += 1 if isinstance(hunk_line, (InsertLine, ContextLine)): mod_size += 1 if hunk is not None: yield hunk class Patch: def __init__(self, oldname, newname): self.oldname = oldname self.newname = newname self.hunks = [] def __str__(self): ret = self.get_header() ret += "".join([str(h) for h in self.hunks]) return ret def get_header(self): return "--- %s\n+++ %s\n" % (self.oldname, self.newname) def stats_str(self): """Return a string of patch statistics""" removes = 0 inserts = 0 for hunk in self.hunks: for line in hunk.lines: if isinstance(line, InsertLine): inserts+=1; elif isinstance(line, RemoveLine): removes+=1; return "%i inserts, %i removes in %i hunks" % \ (inserts, removes, len(self.hunks)) def pos_in_mod(self, position): newpos = position for hunk in self.hunks: shift = hunk.shift_to_mod(position) if shift is None: return None newpos += shift return newpos def iter_inserted(self): """Iteraties through inserted lines :return: Pair of line number, line :rtype: iterator of (int, InsertLine) """ for hunk in self.hunks: pos = hunk.mod_pos - 1; for line in hunk.lines: if isinstance(line, InsertLine): yield (pos, line) pos += 1 if isinstance(line, ContextLine): pos += 1 def parse_patch(iter_lines): (orig_name, mod_name) = get_patch_names(iter_lines) patch = Patch(orig_name, mod_name) for hunk in iter_hunks(iter_lines): patch.hunks.append(hunk) return patch def iter_file_patch(iter_lines): saved_lines = [] for line in iter_lines: if line.startswith('=== '): continue elif line.startswith('--- '): if len(saved_lines) > 0: yield saved_lines saved_lines = [] saved_lines.append(line) if len(saved_lines) > 0: yield saved_lines def iter_lines_handle_nl(iter_lines): """ Iterates through lines, ensuring that lines that originally had no terminating \n are produced without one. This transformation may be applied at any point up until hunk line parsing, and is safe to apply repeatedly. """ last_line = None for line in iter_lines: if line == NO_NL: assert last_line.endswith('\n') last_line = last_line[:-1] line = None if last_line is not None: yield last_line last_line = line if last_line is not None: yield last_line def parse_patches(iter_lines): iter_lines = iter_lines_handle_nl(iter_lines) return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] def difference_index(atext, btext): """Find the indext of the first character that differs betweeen two texts :param atext: The first text :type atext: str :param btext: The second text :type str: str :return: The index, or None if there are no differences within the range :rtype: int or NoneType """ length = len(atext) if len(btext) < length: length = len(btext) for i in range(length): if atext[i] != btext[i]: return i; return None class PatchConflict(Exception): def __init__(self, line_no, orig_line, patch_line): orig = orig_line.rstrip('\n') patch = str(patch_line).rstrip('\n') msg = 'Text contents mismatch at line %d. Original has "%s",'\ ' but patch says it should be "%s"' % (line_no, orig, patch) Exception.__init__(self, msg) def iter_patched(orig_lines, patch_lines): """Iterate through a series of lines with a patch applied. This handles a single file, and does exact, not fuzzy patching. """ if orig_lines is not None: orig_lines = orig_lines.__iter__() seen_patch = [] patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) get_patch_names(patch_lines) line_no = 1 for hunk in iter_hunks(patch_lines): while line_no < hunk.orig_pos: orig_line = orig_lines.next() yield orig_line line_no += 1 for hunk_line in hunk.lines: seen_patch.append(str(hunk_line)) if isinstance(hunk_line, InsertLine): yield hunk_line.contents elif isinstance(hunk_line, (ContextLine, RemoveLine)): orig_line = orig_lines.next() if orig_line != hunk_line.contents: raise PatchConflict(line_no, orig_line, "".join(seen_patch)) if isinstance(hunk_line, ContextLine): yield orig_line else: assert isinstance(hunk_line, RemoveLine) line_no += 1 import unittest import os.path class PatchesTester(unittest.TestCase): def datafile(self, filename): data_path = os.path.join(os.path.dirname(__file__), "testdata", filename) return file(data_path, "rb") def testValidPatchHeader(self): """Parse a valid patch header""" lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') (orig, mod) = get_patch_names(lines.__iter__()) assert(orig == "orig/commands.py") assert(mod == "mod/dommands.py") def testInvalidPatchHeader(self): """Parse an invalid patch header""" lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') self.assertRaises(MalformedPatchHeader, get_patch_names, lines.__iter__()) def testValidHunkHeader(self): """Parse a valid hunk header""" header = "@@ -34,11 +50,6 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 34) assert (hunk.orig_range == 11) assert (hunk.mod_pos == 50) assert (hunk.mod_range == 6) assert (str(hunk) == header) def testValidHunkHeader2(self): """Parse a tricky, valid hunk header""" header = "@@ -1 +0,0 @@\n" hunk = hunk_from_header(header); assert (hunk.orig_pos == 1) assert (hunk.orig_range == 1) assert (hunk.mod_pos == 0) assert (hunk.mod_range == 0) assert (str(hunk) == header) def makeMalformed(self, header): self.assertRaises(MalformedHunkHeader, hunk_from_header, header) def testInvalidHeader(self): """Parse an invalid hunk header""" self.makeMalformed(" -34,11 +50,6 \n") self.makeMalformed("@@ +50,6 -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6 @@") self.makeMalformed("@@ -34.5,11 +50,6 @@\n") self.makeMalformed("@@-34,11 +50,6@@\n") self.makeMalformed("@@ 34,11 50,6 @@\n") self.makeMalformed("@@ -34,11 @@\n") self.makeMalformed("@@ -34,11 +50,6.5 @@\n") self.makeMalformed("@@ -34,11 +50,-6 @@\n") def lineThing(self,text, type): line = parse_line(text) assert(isinstance(line, type)) assert(str(line)==text) def makeMalformedLine(self, text): self.assertRaises(MalformedLine, parse_line, text) def testValidLine(self): """Parse a valid hunk line""" self.lineThing(" hello\n", ContextLine) self.lineThing("+hello\n", InsertLine) self.lineThing("-hello\n", RemoveLine) def testMalformedLine(self): """Parse invalid valid hunk lines""" self.makeMalformedLine("hello\n") def compare_parsed(self, patchtext): lines = patchtext.splitlines(True) patch = parse_patch(lines.__iter__()) pstr = str(patch) i = difference_index(patchtext, pstr) if i is not None: print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) self.assertEqual (patchtext, str(patch)) def testAll(self): """Test parsing a whole patch""" patchtext = """--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: """ self.compare_parsed(patchtext) def testInit(self): """Handle patches missing half the position, range tuple""" patchtext = \ """--- orig/__init__.py +++ mod/__init__.py @@ -1 +1,2 @@ __docformat__ = "restructuredtext en" +__doc__ = An alternate Arch commandline interface """ self.compare_parsed(patchtext) def testLineLookup(self): import sys """Make sure we can accurately look up mod line from orig""" patch = parse_patch(self.datafile("diff")) orig = list(self.datafile("orig")) mod = list(self.datafile("mod")) removals = [] for i in range(len(orig)): mod_pos = patch.pos_in_mod(i) if mod_pos is None: removals.append(orig[i]) continue assert(mod[mod_pos]==orig[i]) rem_iter = removals.__iter__() for hunk in patch.hunks: for line in hunk.lines: if isinstance(line, RemoveLine): next = rem_iter.next() if line.contents != next: sys.stdout.write(" orig:%spatch:%s" % (next, line.contents)) assert(line.contents == next) self.assertRaises(StopIteration, rem_iter.next) def testFirstLineRenumber(self): """Make sure we handle lines at the beginning of the hunk""" patch = parse_patch(self.datafile("insert_top.patch")) assert (patch.pos_in_mod(0)==1) def test(): patchesTestSuite = unittest.makeSuite(PatchesTester,'test') runner = unittest.TextTestRunner(verbosity=0) return runner.run(patchesTestSuite) if __name__ == "__main__": test() # arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 patchkit-0.1.8/test_patches_data/orig-7000064400000000000000000000000261046102023000161450ustar 00000000000000No terminating newlinepatchkit-0.1.8/test_patches_data/patchtext.patch000064400000000000000000000017341046102023000201520ustar 00000000000000--- orig/commands.py +++ mod/commands.py @@ -1337,7 +1337,8 @@ def set_title(self, command=None): try: - version = self.tree.tree_version.nonarch + version = pylon.alias_or_version(self.tree.tree_version, self.tree, + full=False) except: version = "[no version]" if command is None: @@ -1983,7 +1984,11 @@ version) if len(new_merges) > 0: if cmdutil.prompt("Log for merge"): - mergestuff = cmdutil.log_for_merge(tree, comp_version) + if cmdutil.prompt("changelog for merge"): + mergestuff = "Patches applied:\\n" + mergestuff += pylon.changelog_for_merge(new_merges) + else: + mergestuff = cmdutil.log_for_merge(tree, comp_version) log.description += mergestuff log.save() try: