Beispiel #1
0
    def test_get_unstaged_changes(self):
        """Unit test for get_unstaged_changes."""

        repo_dir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, repo_dir)
        with Repo.init(repo_dir) as repo:

            # Commit a dummy file then modify it
            foo1_fullpath = os.path.join(repo_dir, 'foo1')
            with open(foo1_fullpath, 'wb') as f:
                f.write(b'origstuff')

            foo2_fullpath = os.path.join(repo_dir, 'foo2')
            with open(foo2_fullpath, 'wb') as f:
                f.write(b'origstuff')

            repo.stage(['foo1', 'foo2'])
            repo.do_commit(b'test status',
                           author=b'author <email>',
                           committer=b'committer <email>')

            with open(foo1_fullpath, 'wb') as f:
                f.write(b'newstuff')

            # modify access and modify time of path
            os.utime(foo1_fullpath, (0, 0))

            changes = get_unstaged_changes(repo.open_index(), repo_dir)

            self.assertEqual(list(changes), [b'foo1'])
Beispiel #2
0
    def test_get_unstaged_changes_removed_replaced_by_link(self):
        """Unit test for get_unstaged_changes."""

        repo_dir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, repo_dir)
        with Repo.init(repo_dir) as repo:

            # Commit a dummy file then modify it
            foo1_fullpath = os.path.join(repo_dir, "foo1")
            with open(foo1_fullpath, "wb") as f:
                f.write(b"origstuff")

            repo.stage(["foo1"])
            repo.do_commit(
                b"test status",
                author=b"author <email>",
                committer=b"committer <email>",
            )

            os.remove(foo1_fullpath)
            os.symlink(os.path.dirname(foo1_fullpath), foo1_fullpath)

            changes = get_unstaged_changes(repo.open_index(), repo_dir)

            self.assertEqual(list(changes), [b"foo1"])
Beispiel #3
0
def status(repo=".", ignored=False):
    """Returns staged, unstaged, and untracked changes relative to the HEAD.

    :param repo: Path to repository or repository object
    :param ignored: Whether to include ignored files in `untracked`
    :return: GitStatus tuple,
        staged -    list of staged paths (diff index/HEAD)
        unstaged -  list of unstaged paths (diff index/working-tree)
        untracked - list of untracked, un-ignored & non-.git paths
    """
    with open_repo_closing(repo) as r:
        # 1. Get status of staged
        tracked_changes = get_tree_changes(r)
        # 2. Get status of unstaged
        index = r.open_index()
        normalizer = r.get_blob_normalizer()
        filter_callback = normalizer.checkin_normalize
        unstaged_changes = list(
            get_unstaged_changes(index, r.path, filter_callback))
        ignore_manager = IgnoreFilterManager.from_repo(r)
        untracked_paths = get_untracked_paths(r.path, r.path, index)
        if ignored:
            untracked_changes = list(untracked_paths)
        else:
            untracked_changes = [
                p for p in untracked_paths if not ignore_manager.is_ignored(p)
            ]
        return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
Beispiel #4
0
    def test_get_unstaged_changes(self):
        """Unit test for get_unstaged_changes."""

        repo_dir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, repo_dir)
        with Repo.init(repo_dir) as repo:

            # Commit a dummy file then modify it
            foo1_fullpath = os.path.join(repo_dir, 'foo1')
            with open(foo1_fullpath, 'wb') as f:
                f.write(b'origstuff')

            foo2_fullpath = os.path.join(repo_dir, 'foo2')
            with open(foo2_fullpath, 'wb') as f:
                f.write(b'origstuff')

            repo.stage(['foo1', 'foo2'])
            repo.do_commit(b'test status', author=b'', committer=b'')

            with open(foo1_fullpath, 'wb') as f:
                f.write(b'newstuff')

            # modify access and modify time of path
            os.utime(foo1_fullpath, (0, 0))

            changes = get_unstaged_changes(repo.open_index(), repo_dir)

            self.assertEqual(list(changes), [b'foo1'])
Beispiel #5
0
    def test_get_unstaged_changes(self):
        """Unit test for get_unstaged_changes."""

        repo_dir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, repo_dir)
        with Repo.init(repo_dir) as repo:

            # Commit a dummy file then modify it
            foo1_fullpath = os.path.join(repo_dir, "foo1")
            with open(foo1_fullpath, "wb") as f:
                f.write(b"origstuff")

            foo2_fullpath = os.path.join(repo_dir, "foo2")
            with open(foo2_fullpath, "wb") as f:
                f.write(b"origstuff")

            repo.stage(["foo1", "foo2"])
            repo.do_commit(
                b"test status",
                author=b"author <email>",
                committer=b"committer <email>",
            )

            with open(foo1_fullpath, "wb") as f:
                f.write(b"newstuff")

            # modify access and modify time of path
            os.utime(foo1_fullpath, (0, 0))

            changes = get_unstaged_changes(repo.open_index(), repo_dir)

            self.assertEqual(list(changes), [b"foo1"])
Beispiel #6
0
    def test_get_unstaged_changes(self):
        """Unit test for get_unstaged_changes."""

        repo_dir = tempfile.mkdtemp()
        repo = Repo.init(repo_dir)
        self.addCleanup(shutil.rmtree, repo_dir)

        # Commit a dummy file then modify it
        foo1_fullpath = os.path.join(repo_dir, "foo1")
        with open(foo1_fullpath, "w") as f:
            f.write("origstuff")

        foo2_fullpath = os.path.join(repo_dir, "foo2")
        with open(foo2_fullpath, "w") as f:
            f.write("origstuff")

        repo.stage(["foo1", "foo2"])
        repo.do_commit("test status", author="", committer="")

        with open(foo1_fullpath, "w") as f:
            f.write("newstuff")

        # modify access and modify time of path
        os.utime(foo1_fullpath, (0, 0))

        changes = get_unstaged_changes(repo.open_index(), repo_dir)

        self.assertEqual(list(changes), ["foo1"])
Beispiel #7
0
def status(repo=".", ignored=False):
    """Returns staged, unstaged, and untracked changes relative to the HEAD.

    :param repo: Path to repository or repository object
    :param ignored: Whether to include ignored files in `untracked`
    :return: GitStatus tuple,
        staged -    list of staged paths (diff index/HEAD)
        unstaged -  list of unstaged paths (diff index/working-tree)
        untracked - list of untracked, un-ignored & non-.git paths
    """
    with open_repo_closing(repo) as r:
        # 1. Get status of staged
        tracked_changes = get_tree_changes(r)
        # 2. Get status of unstaged
        index = r.open_index()
        normalizer = r.get_blob_normalizer()
        filter_callback = normalizer.checkin_normalize
        unstaged_changes = list(
            get_unstaged_changes(index, r.path, filter_callback)
        )
        ignore_manager = IgnoreFilterManager.from_repo(r)
        untracked_paths = get_untracked_paths(r.path, r.path, index)
        if ignored:
            untracked_changes = list(untracked_paths)
        else:
            untracked_changes = [
                    p for p in untracked_paths
                    if not ignore_manager.is_ignored(p)]
        return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
Beispiel #8
0
 def fetch_unstaged_changes(self):
     """ Fetch the current list of unstaged changes from git. """
     self.unstaged_changes = []
     try:
         for change in get_unstaged_changes(
                 self.git_repository.open_index(), self.physical_path):
             self.unstaged_changes.append(change.decode('utf-8'))
     except FileNotFoundError:
         pass
def status(repo):
    """Returns staged, unstaged, and untracked changes relative to the HEAD.

    :param repo: Path to repository
    :return: GitStatus tuple,
        staged -    list of staged paths (diff index/HEAD)
        unstaged -  list of unstaged paths (diff index/working-tree)
        untracked - list of untracked, un-ignored & non-.git paths
    """
    # 1. Get status of staged
    tracked_changes = get_tree_changes(repo)
    # 2. Get status of unstaged
    unstaged_changes = list(get_unstaged_changes(repo.open_index(), repo.path))
    # TODO - Status of untracked - add untracked changes, need gitignore.
    untracked_changes = []
    return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
Beispiel #10
0
def status(repo="."):
    """Returns staged, unstaged, and untracked changes relative to the HEAD.

    :param repo: Path to repository or repository object
    :return: GitStatus tuple,
        staged -    list of staged paths (diff index/HEAD)
        unstaged -  list of unstaged paths (diff index/working-tree)
        untracked - list of untracked, un-ignored & non-.git paths
    """
    with open_repo_closing(repo) as r:
        # 1. Get status of staged
        tracked_changes = get_tree_changes(r)
        # 2. Get status of unstaged
        index = r.open_index()
        unstaged_changes = list(get_unstaged_changes(index, r.path))
        untracked_changes = list(get_untracked_paths(r.path, r.path, index))
        return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
Beispiel #11
0
    def test_get_unstaged_deleted_changes(self):
        """Unit test for get_unstaged_changes."""

        repo_dir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, repo_dir)
        with Repo.init(repo_dir) as repo:

            # Commit a dummy file then remove it
            foo1_fullpath = os.path.join(repo_dir, 'foo1')
            with open(foo1_fullpath, 'wb') as f:
                f.write(b'origstuff')

            repo.stage(['foo1'])
            repo.do_commit(b'test status', author=b'', committer=b'')

            os.unlink(foo1_fullpath)

            changes = get_unstaged_changes(repo.open_index(), repo_dir)

            self.assertEqual(list(changes), [b'foo1'])
Beispiel #12
0
    def test_get_unstaged_deleted_changes(self):
        """Unit test for get_unstaged_changes."""

        repo_dir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, repo_dir)
        with Repo.init(repo_dir) as repo:

            # Commit a dummy file then remove it
            foo1_fullpath = os.path.join(repo_dir, 'foo1')
            with open(foo1_fullpath, 'wb') as f:
                f.write(b'origstuff')

            repo.stage(['foo1'])
            repo.do_commit(b'test status', author=b'', committer=b'')

            os.unlink(foo1_fullpath)

            changes = get_unstaged_changes(repo.open_index(), repo_dir)

            self.assertEqual(list(changes), [b'foo1'])
Beispiel #13
0
    def test_get_unstaged_changes_removed_replaced_by_directory(self):
        """Unit test for get_unstaged_changes."""

        repo_dir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, repo_dir)
        with Repo.init(repo_dir) as repo:

            # Commit a dummy file then modify it
            foo1_fullpath = os.path.join(repo_dir, 'foo1')
            with open(foo1_fullpath, 'wb') as f:
                f.write(b'origstuff')

            repo.stage(['foo1'])
            repo.do_commit(b'test status', author=b'author <email>',
                           committer=b'committer <email>')

            os.remove(foo1_fullpath)
            os.mkdir(foo1_fullpath)

            changes = get_unstaged_changes(repo.open_index(), repo_dir)

            self.assertEqual(list(changes), [b'foo1'])
Beispiel #14
0
    def test_get_unstaged_changes_removed_replaced_by_link(self):
        """Unit test for get_unstaged_changes."""

        repo_dir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, repo_dir)
        with Repo.init(repo_dir) as repo:

            # Commit a dummy file then modify it
            foo1_fullpath = os.path.join(repo_dir, 'foo1')
            with open(foo1_fullpath, 'wb') as f:
                f.write(b'origstuff')

            repo.stage(['foo1'])
            repo.do_commit(b'test status', author=b'author <email>',
                           committer=b'committer <email>')

            os.remove(foo1_fullpath)
            os.symlink(os.path.dirname(foo1_fullpath), foo1_fullpath)

            changes = get_unstaged_changes(repo.open_index(), repo_dir)

            self.assertEqual(list(changes), [b'foo1'])
def describe(repo, **kwargs):
    """Describe the repository version.

    :param projdir: git repository root
    :returns: a string description of the current git revision

    Examples: "gabcdefh", "v0.1" or "v0.1-5-gabcdefh".
    """

    # Set all options to the default values.
    option_abbrev = 7
    option_long = False
    option_dirty = ''

    if kwargs is not None:
        for key, value in kwargs.iteritems():
            if key == 'abbrev':
                option_abbrev = int(value)
            elif key == 'long':
                option_long = bool(value)
            elif key == 'dirty':
                option_dirty = str(value)
            else:
                raise Exception('Unknown keyword parameter "%s".' % key)

    # Get the repository
    with open_repo_closing(repo) as r:
        # Check if the repository is dirty.
        tracked_changes = get_tree_changes(r)
        index = r.open_index()
        unstaged_changes = list(get_unstaged_changes(index, r.path))
        if(
            len(tracked_changes['add']) == 0 and
            len(tracked_changes['modify']) == 0 and
            len(tracked_changes['delete']) == 0 and
            len(unstaged_changes) == 0
        ):
            dirty = ''
        else:
            dirty = option_dirty

        # Get a list of all tags
        refs = r.get_refs()
        tags = {}
        for key, value in refs.items():
            key = key.decode()
            obj = r.get_object(value)
            if u'tags' not in key:
                continue

            _, tag = key.rsplit(u'/', 1)

            try:
                commit = obj.object
            except AttributeError:
                continue
            else:
                commit = r.get_object(commit[1])
            tags[tag] = [
                datetime.datetime(*time.gmtime(commit.commit_time)[:6]),
                commit.id.decode('ascii'),
            ]

        sorted_tags = sorted(tags.items(),
                             key=lambda tag: tag[1][0],
                             reverse=True)

        # If there are no tags, return the current commit
        if len(sorted_tags) == 0:
            return '{}{}'.format(
                r[r.head()].id.decode('ascii')[:option_abbrev],
                dirty
            )

        # We're now 0 commits from the top
        commit_count = 0

        # Get the latest commit
        latest_commit = r[r.head()]

        # Walk through all commits
        walker = r.get_walker()
        for entry in walker:
            # Check if tag
            commit_id = entry.commit.id.decode('ascii')
            for tag in sorted_tags:
                tag_name = tag[0]
                tag_commit = tag[1][1]
                if commit_id == tag_commit:
                    if (commit_count == 0) and (option_long is not True):
                        return '{}{}'.format(tag_name, dirty)
                    else:
                        return '{}-{}-g{}{}'.format(
                            tag_name,
                            commit_count,
                            latest_commit.id.decode('ascii')[:option_abbrev],
                            dirty
                        )

            commit_count += 1

        # Return plain commit if no parent tag can be found
        return '{}{}'.format(
            latest_commit.id.decode('ascii')[:option_abbrev],
            dirty
        )
Beispiel #16
0
def describe(repo, **kwargs):
    """Describe the repository version.

    :param projdir: git repository root
    :returns: a string description of the current git revision

    Examples: "gabcdefh", "v0.1" or "v0.1-5-gabcdefh".
    """

    # Set all options to the default values.
    option_abbrev = 7
    option_long = False
    option_dirty = ''

    if kwargs is not None:
        for key, value in kwargs.iteritems():
            if key == 'abbrev':
                option_abbrev = int(value)
            elif key == 'long':
                option_long = bool(value)
            elif key == 'dirty':
                option_dirty = str(value)
            else:
                raise Exception('Unknown keyword parameter "%s".' % key)

    # Get the repository
    with open_repo_closing(repo) as r:
        # Check if the repository is dirty.
        tracked_changes = get_tree_changes(r)
        index = r.open_index()
        unstaged_changes = list(get_unstaged_changes(index, r.path))
        if (len(tracked_changes['add']) == 0
                and len(tracked_changes['modify']) == 0
                and len(tracked_changes['delete']) == 0
                and len(unstaged_changes) == 0):
            dirty = ''
        else:
            dirty = option_dirty

        # Get a list of all tags
        refs = r.get_refs()
        tags = {}
        for key, value in refs.items():
            key = key.decode()
            obj = r.get_object(value)
            if u'tags' not in key:
                continue

            _, tag = key.rsplit(u'/', 1)

            try:
                commit = obj.object
            except AttributeError:
                continue
            else:
                commit = r.get_object(commit[1])
            tags[tag] = [
                datetime.datetime(*time.gmtime(commit.commit_time)[:6]),
                commit.id.decode('ascii'),
            ]

        sorted_tags = sorted(tags.items(),
                             key=lambda tag: tag[1][0],
                             reverse=True)

        # If there are no tags, return the current commit
        if len(sorted_tags) == 0:
            return '{}{}'.format(
                r[r.head()].id.decode('ascii')[:option_abbrev], dirty)

        # We're now 0 commits from the top
        commit_count = 0

        # Get the latest commit
        latest_commit = r[r.head()]

        # Walk through all commits
        walker = r.get_walker()
        for entry in walker:
            # Check if tag
            commit_id = entry.commit.id.decode('ascii')
            for tag in sorted_tags:
                tag_name = tag[0]
                tag_commit = tag[1][1]
                if commit_id == tag_commit:
                    if (commit_count == 0) and (option_long is not True):
                        return '{}{}'.format(tag_name, dirty)
                    else:
                        return '{}-{}-g{}{}'.format(
                            tag_name, commit_count,
                            latest_commit.id.decode('ascii')[:option_abbrev],
                            dirty)

            commit_count += 1

        # Return plain commit if no parent tag can be found
        return '{}{}'.format(
            latest_commit.id.decode('ascii')[:option_abbrev], dirty)
Beispiel #17
0
    def get_notes_by_version(self):
        """Return an OrderedDict mapping versions to lists of notes files.

        The versions are presented in reverse chronological order.

        Notes files are associated with the earliest version for which
        they were available, regardless of whether they changed later.

        :param reporoot: Path to the root of the git repository.
        :type reporoot: str
        """

        reporoot = self.reporoot
        notesdir = self.conf.notespath
        branch = self.conf.branch
        earliest_version = self.conf.earliest_version
        collapse_pre_releases = self.conf.collapse_pre_releases
        stop_at_branch_base = self.conf.stop_at_branch_base

        LOG.info('scanning %s/%s (branch=%s earliest_version=%s)',
                 reporoot.rstrip('/'), notesdir.lstrip('/'),
                 branch or '*current*', earliest_version)

        # Determine the current version, which might be an unreleased or
        # dev version if there are unreleased commits at the head of the
        # branch in question.
        current_version = self._get_current_version(branch)
        LOG.debug('current repository version: %s' % current_version)

        # Determine all of the tags known on the branch, in their date
        # order. We scan the commit history in topological order to ensure
        # we have the commits in the right version, so we might encounter
        # the tags in a different order during that phase.
        versions_by_date = self._get_tags_on_branch(branch)
        LOG.debug('versions by date %r' % (versions_by_date,))
        if earliest_version and earliest_version not in versions_by_date:
            raise ValueError(
                'earliest-version set to unknown revision {!r}'.format(
                    earliest_version))

        # If the user has told us where to stop, use that as the
        # default.
        scan_stop_tag = self._find_scan_stop_point(
            earliest_version, versions_by_date,
            collapse_pre_releases, branch)

        # If the user has not told us where to stop, try to work it
        # out for ourselves.
        if not branch and not earliest_version and stop_at_branch_base:
            # On the current branch, stop at the point where the most
            # recent branch was created, if we can find one.
            LOG.debug('working on current branch without earliest_version')
            branches = self._get_series_branches()
            if branches:
                LOG.debug('looking at base of %s to stop scanning master',
                          branches[-1])
                scan_stop_tag = self._get_branch_base(branches[-1])
                earliest_version = current_version
        elif branch and stop_at_branch_base and not earliest_version:
            # If branch is set and is not "master",
            # then we want to stop at the version before the tag at the
            # base of the branch, which involves a bit of searching.
            LOG.debug('determining earliest_version from branch')
            branch_base = self._get_branch_base(branch)
            scan_stop_tag = self._find_scan_stop_point(
                branch_base, versions_by_date,
                collapse_pre_releases, branch)
            if not scan_stop_tag:
                earliest_version = branch_base
            else:
                idx = versions_by_date.index(scan_stop_tag)
                earliest_version = versions_by_date[idx - 1]
            if earliest_version and collapse_pre_releases:
                if self.pre_release_tag_re.search(earliest_version):
                    # The earliest version won't actually be the pre-release
                    # that might have been tagged when the branch was created,
                    # but the final version. Strip the pre-release portion of
                    # the version number.
                    earliest_version = self._strip_pre_release(
                        earliest_version
                    )
        if earliest_version:
            LOG.info('earliest version to include is %s', earliest_version)
        else:
            LOG.info('including entire branch history')
        if scan_stop_tag:
            LOG.info('stopping scan at %s', scan_stop_tag)

        # Since the version may not already be known, make sure it is
        # in the list of versions by date. And since it is the most
        # recent version, go ahead and insert it at the front of the
        # list.
        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')
                if fname.startswith(prefix) and _note_file(fname):
                    tracker.add(fname, None, '*working-copy*')
            for fname in changes['modify']:
                fname = fname.decode('utf-8')
                if fname.startswith(prefix) and _note_file(fname):
                    tracker.modify(fname, None, '*working-copy*')
            for fname in changes['delete']:
                fname = fname.decode('utf-8')
                if fname.startswith(prefix) and _note_file(fname):
                    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
            tags_on_commit = self._get_valid_tags_on_commit(sha)

            LOG.debug('%06d %s %s', counter, sha, tags_on_commit)

            # If there are no tags in this block, assume the most recently
            # seen version.
            tags = tags_on_commit
            if not tags:
                tags = [current_version]
            else:
                current_version = tags_on_commit[-1]
                LOG.info('%06d %s updating current version to %s',
                         counter, sha, current_version)

            # Look for changes to notes files in this commit. The
            # change has only the basename of the path file, so we
            # need to prefix that with the notesdir before giving it
            # to the tracker.
            changes = _changes_in_subdir(self._repo, entry, notesdir)
            for change in _aggregate_changes(entry, changes, notesdir):
                uniqueid = change[0]

                c_type = change[1]

                if c_type == diff_tree.CHANGE_ADD:
                    path, blob_sha = change[-2:]
                    fullpath = os.path.join(notesdir, path)
                    tracker.add(fullpath, sha, current_version)

                elif c_type == diff_tree.CHANGE_DELETE:
                    path = change[-1]
                    fullpath = os.path.join(notesdir, path)
                    tracker.delete(fullpath, sha, current_version)

                elif c_type == diff_tree.CHANGE_RENAME:
                    path, blob_sha = change[-2:]
                    fullpath = os.path.join(notesdir, path)
                    tracker.rename(fullpath, sha, current_version)

                elif c_type == diff_tree.CHANGE_MODIFY:
                    path, blob_sha = change[-2:]
                    fullpath = os.path.join(notesdir, path)
                    tracker.modify(fullpath, sha, current_version)

                else:
                    raise ValueError(
                        'unknown change instructions {!r}'.format(change)
                    )

            if scan_stop_tag and scan_stop_tag in tags:
                LOG.info(
                    ('reached end of branch after %d commits at %s '
                     'with tags %s'),
                    counter, sha, tags)
                break

        # Invert earliest_seen to make a list of notes files for each
        # version.
        files_and_tags = collections.OrderedDict()
        for v in tracker.versions:
            files_and_tags[v] = []
        # Produce a list of the actual files present in the repository. If
        # a note is removed, this step should let us ignore it.
        for uniqueid, version in tracker.earliest_seen.items():
            try:
                base, sha = tracker.last_name_by_id[uniqueid]
                LOG.debug('%s: sorting %s into version %s',
                          uniqueid, base, version)
                files_and_tags[version].append((base, sha))
            except KeyError:
                # Unable to find the file again, skip it to avoid breaking
                # the build.
                msg = ('unable to find release notes file associated '
                       'with unique id %r, skipping') % uniqueid
                LOG.debug(msg)
                print(msg, file=sys.stderr)

        # Combine pre-releases into the final release, if we are told to
        # and the final release exists.
        if collapse_pre_releases:
            LOG.debug('collapsing pre-release versions into final releases')
            collapsing = files_and_tags
            files_and_tags = collections.OrderedDict()
            for ov in versions_by_date:
                if ov not in collapsing:
                    # We don't need to collapse this one because there are
                    # no notes attached to it.
                    continue
                pre_release_match = self.pre_release_tag_re.search(ov)
                LOG.debug('checking %r', ov)
                if pre_release_match:
                    # Remove the trailing pre-release part of the version
                    # from the string.
                    canonical_ver = self._strip_pre_release(ov)
                    if canonical_ver not in versions_by_date:
                        # This canonical version was never tagged, so we
                        # do not want to collapse the pre-releases. Reset
                        # to the original version.
                        canonical_ver = ov
                    else:
                        LOG.debug('combining into %r', canonical_ver)
                else:
                    canonical_ver = ov
                if canonical_ver not in files_and_tags:
                    files_and_tags[canonical_ver] = []
                files_and_tags[canonical_ver].extend(collapsing[ov])

        LOG.debug('files_and_tags %s',
                  {k: len(v) for k, v in files_and_tags.items()})
        # Only return the parts of files_and_tags that actually have
        # filenames associated with the versions.
        LOG.debug('trimming')
        trimmed = collections.OrderedDict()
        for ov in versions_by_date:
            if not files_and_tags.get(ov):
                continue
            LOG.debug('keeping %s', ov)
            # Sort the notes associated with the version so they are in a
            # deterministic order, to avoid having the same data result in
            # different output depending on random factors. Earlier
            # versions of the scanner assumed the notes were recorded in
            # chronological order based on the commit date, but with the
            # change to use topological sorting that is no longer
            # necessarily true. We want the notes to always show up in the
            # same order, but it doesn't really matter what order that is,
            # so just sort based on the unique id.
            trimmed[ov] = sorted(files_and_tags[ov])
            # If we have been told to stop at a version, we can do that
            # now.
            if earliest_version and ov == earliest_version:
                LOG.debug('stopping trimming at %s', earliest_version)
                break

        LOG.debug(
            'found %d versions and %d files',
            len(trimmed.keys()), sum(len(ov) for ov in trimmed.values()),
        )
        return trimmed