From f8fc8f97ff20026582742e3e7838cdd0ed5cad68 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 22 Dec 2016 16:47:19 -0500 Subject: [PATCH] teach the scanner to look at uncommitted files Update the Scanner to look at staged and unstaged changes to files in the local copy of the repository. We can't detect unknown files, yet. Closes-Bug: #1553155 Change-Id: I77ed60e6f8b8f819aabb361f34cf779623907f7b Signed-off-by: Doug Hellmann --- ...include-working-copy-d0aed2e77bb095e6.yaml | 7 ++ reno/scanner.py | 58 +++++++++++- reno/tests/test_scanner.py | 90 +++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/include-working-copy-d0aed2e77bb095e6.yaml diff --git a/releasenotes/notes/include-working-copy-d0aed2e77bb095e6.yaml b/releasenotes/notes/include-working-copy-d0aed2e77bb095e6.yaml new file mode 100644 index 0000000..5a02790 --- /dev/null +++ b/releasenotes/notes/include-working-copy-d0aed2e77bb095e6.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Include the local working copy when scanning the history of the + current branch. Notes files must at least be staged to indicate + that they will eventually be part of the history, but subsequent + changes to the file do not need to also be staged to be seen. diff --git a/reno/scanner.py b/reno/scanner.py index e02ed17..713b00f 100644 --- a/reno/scanner.py +++ b/reno/scanner.py @@ -20,7 +20,9 @@ import re import sys from dulwich import diff_tree +from dulwich import index as d_index from dulwich import objects +from dulwich import porcelain from dulwich import repo LOG = logging.getLogger(__name__) @@ -423,7 +425,23 @@ class RenoRepo(repo.Repo): return tree def get_file_at_commit(self, filename, sha): - "Return the contents of the file if it exists at the commit, or None." + """Return the contents of the file. + + If sha is None, return the working copy of the file. If the + file cannot be read from the working dir, return None. + + If the sha is not None and the file exists at the commit, + return the data from the stored blob. If the file does not + exist at the commit, return None. + + """ + if sha is None: + # Get the copy from the working directory. + try: + with open(os.path.join(self.path, filename), 'r') as f: + return f.read() + except IOError: + return None # Get the tree associated with the commit identified by the # input SHA, then look through the items in the tree to find # the one with the path matching the filename. Take the @@ -763,10 +781,48 @@ class Scanner(object): LOG.debug('current repository version: %s' % current_version) if current_version not in versions_by_date: versions_by_date.insert(0, current_version) + versions_by_date.insert(0, '*working-copy*') # Track the versions we have seen and the earliest version for # which we have seen a given note's unique id. tracker = _ChangeTracker() + + # Process the local index, if we are scanning the current + # branch. + if not branch: + prefix = notesdir.rstrip('/') + '/' + index = self._repo.open_index() + + # Pretend anything known to the repo and changed but not + # staged is part of the fake version '*working-copy*'. + LOG.debug('scanning unstaged changes') + for fname in d_index.get_unstaged_changes(index, self.reporoot): + fname = fname.decode('utf-8') + LOG.debug('found unstaged file %s', fname) + if fname.startswith(prefix) and _note_file(fname): + fullpath = os.path.join(self.reporoot, fname) + if os.path.exists(fullpath): + LOG.debug('found file %s', fullpath) + tracker.add(fname, None, '*working-copy*') + else: + LOG.debug('deleted file %s', fullpath) + tracker.delete(fname, None, '*working-copy*') + + # Pretend anything in the index is part of the fake + # version "*working-copy*". + LOG.debug('scanning staged schanges') + changes = porcelain.get_tree_changes(self._repo) + for fname in changes['add']: + fname = fname.decode('utf-8') + tracker.add(fname, None, '*working-copy*') + for fname in changes['modify']: + fname = fname.decode('utf-8') + tracker.modify(fname, None, '*working-copy*') + for fname in changes['delete']: + fname = fname.decode('utf-8') + tracker.delete(fname, None, '*working-copy*') + + # Process the git commit history. for counter, entry in enumerate(self._topo_traversal(branch), 1): sha = entry.commit.id diff --git a/reno/tests/test_scanner.py b/reno/tests/test_scanner.py index af0ab57..a419104 100644 --- a/reno/tests/test_scanner.py +++ b/reno/tests/test_scanner.py @@ -20,6 +20,7 @@ import os.path import re import subprocess import time +import unittest from dulwich import diff_tree from dulwich import objects @@ -529,6 +530,82 @@ class BasicTest(Base): results, ) + def test_staged_file(self): + # Prove that we can get a file we have staged. + # Start with a standard commit and tag + self._make_python_package() + self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') + # Now stage a release note + n = self.get_note_num() + basename = 'staged-note-%016x.yaml' % n + filename = os.path.join(self.reporoot, 'releasenotes', 'notes', + basename) + create._make_note_file(filename, 'staged note') + self.repo.git('add', filename) + status_results = self.repo.git('status') + self.addDetail('git status', text_content(status_results)) + # Now run the scanner + self.scanner = scanner.Scanner(self.c) + raw_results = self.scanner.get_notes_by_version() + self.assertEqual( + {'*working-copy*': [ + (os.path.join('releasenotes', 'notes', basename), + None)], + }, + raw_results, + ) + + @unittest.skip('dulwich does not know how to identify new files') + def test_added_tagged_not_staged(self): + # Prove that we can get a file we have created but not staged. + # Start with a standard commit and tag + self._make_python_package() + self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') + # Now create a note without staging it + n = self.get_note_num() + basename = 'staged-note-%016x.yaml' % n + filename = os.path.join(self.reporoot, 'releasenotes', 'notes', + basename) + create._make_note_file(filename, 'staged note') + status_results = self.repo.git('status') + self.addDetail('git status', text_content(status_results)) + # Now run the scanner + self.scanner = scanner.Scanner(self.c) + raw_results = self.scanner.get_notes_by_version() + # Take the staged version of the file, but associate it with + # tagged version 1.0.0 because the file was added before that + # version. + self.assertEqual( + {'1.0.0': [(os.path.join('releasenotes', 'notes', basename), + None)], + }, + raw_results, + ) + + def test_modified_tagged_not_staged(self): + # Prove that we can get a file we have changed but not staged. + # Start with a standard commit and tag + self._make_python_package() + f1 = self._add_notes_file('slug1') + self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') + # Now modify the note + fullpath = os.path.join(self.repo.reporoot, f1) + with open(fullpath, 'w') as f: + f.write('modified first note') + status_results = self.repo.git('status') + self.addDetail('git status', text_content(status_results)) + # Now run the scanner + self.scanner = scanner.Scanner(self.c) + raw_results = self.scanner.get_notes_by_version() + # Take the staged version of the file, but associate it with + # tagged version 1.0.0 because the file was added before that + # version. + self.assertEqual( + {'1.0.0': [(f1, None)], + }, + raw_results, + ) + class FileContentsTest(Base): @@ -597,6 +674,19 @@ class FileContentsTest(Base): contents, ) + def test_staged_file(self): + # Prove we are not picking up the contents from the local + # filesystem outside of the git history. + f1 = self._add_notes_file(contents='initial-contents') + with open(os.path.join(self.reporoot, f1), 'w') as f: + f.write('new contents for file') + r = scanner.RenoRepo(self.reporoot) + contents = r.get_file_at_commit(f1, None) + self.assertEqual( + 'new contents for file', + contents, + ) + class PreReleaseTest(Base):