예제 #1
0
class Troll(object):
    def __init__(self, url, args):
        self.url = url
        self.args = args
        self.gerrit = Gerrit(url)
        self.tag = 'autogenerated:review-o-matic'
        self.blacklist = {}
        self.stats = {
            str(ReviewType.SUCCESS): 0,
            str(ReviewType.BACKPORT): 0,
            str(ReviewType.ALTERED_UPSTREAM): 0,
            str(ReviewType.MISSING_FIELDS): 0,
            str(ReviewType.MISSING_HASH): 0,
            str(ReviewType.INVALID_HASH): 0,
            str(ReviewType.MISSING_AM): 0,
            str(ReviewType.INCORRECT_PREFIX): 0,
            str(ReviewType.FIXES_REF): 0
        }

    def inc_stat(self, review_type):
        if self.args.dry_run:
            return
        key = str(review_type)
        if not self.stats.get(key):
            self.stats[key] = 1
        else:
            self.stats[key] += 1

    def do_review(self, change, review):
        print('Review for change: {}'.format(change.url()))
        print('  Issues: {}, Feedback: {}, Vote:{}, Notify:{}'.format(
            review.issues.keys(), review.feedback.keys(), review.vote,
            review.notify))

        if review.dry_run:
            print(review.generate_review_message())
            print('------')
            return

        for i in review.issues:
            self.inc_stat(i)
        for f in review.feedback:
            self.inc_stat(f)
        self.gerrit.review(change,
                           self.tag,
                           review.generate_review_message(),
                           review.notify,
                           vote_code_review=review.vote)

    def get_changes(self, prefix):
        message = '{}:'.format(prefix)
        after = datetime.date.today() - datetime.timedelta(days=5)
        changes = self.gerrit.query_changes(
            status='open',
            message=message,
            after=after,
            project='chromiumos/third_party/kernel')
        return changes

    def add_change_to_blacklist(self, change):
        self.blacklist[change.number] = change.current_revision.number

    def is_change_in_blacklist(self, change):
        return self.blacklist.get(
            change.number) == change.current_revision.number

    def process_changes(self, changes):
        rev = Reviewer(git_dir=self.args.git_dir,
                       verbose=self.args.verbose,
                       chatty=self.args.chatty)
        ret = 0
        for c in changes:
            if self.args.verbose:
                print('Processing change {}'.format(c.url()))

            # Blacklist if we've already reviewed this revision
            for m in c.messages:
                if m.tag == self.tag and m.revision_num == c.current_revision.number:
                    self.add_change_to_blacklist(c)

            # Find a reviewer and blacklist if not found
            reviewer = None
            if FromlistChangeReviewer.can_review_change(c):
                reviewer = FromlistChangeReviewer(rev, c, self.args.dry_run)
            elif FromgitChangeReviewer.can_review_change(c):
                reviewer = FromgitChangeReviewer(rev, c, self.args.dry_run)
            elif UpstreamChangeReviewer.can_review_change(c):
                reviewer = UpstreamChangeReviewer(rev, c, self.args.dry_run)
            if not reviewer:
                self.add_change_to_blacklist(c)
                continue

            force_review = self.args.force_cl or self.args.force_all
            if not force_review and self.is_change_in_blacklist(c):
                continue

            result = reviewer.review_patch()
            if result:
                self.do_review(c, result)
                ret += 1

            self.add_change_to_blacklist(c)

        return ret

    def update_stats(self):
        if not self.args.dry_run and self.args.stats_file:
            with open(self.args.stats_file, 'wt') as f:
                json.dump(self.stats, f)
        print('--')
        summary = '  Summary: '
        total = 0
        for k, v in self.stats.items():
            summary += '{}={} '.format(k, v)
            total += v
        summary += 'total={}'.format(total)
        print(summary)
        print('')

    def run(self):
        if self.args.force_cl:
            c = self.gerrit.get_change(self.args.force_cl)
            print('Force reviewing change  {}'.format(c))
            self.process_changes([c])
            return

        if self.args.stats_file:
            try:
                with open(self.args.stats_file, 'rt') as f:
                    self.stats = json.load(f)
            except FileNotFoundError:
                self.update_stats()

        prefixes = ['UPSTREAM', 'BACKPORT', 'FROMGIT', 'FROMLIST']
        while True:
            try:
                did_review = 0
                for p in prefixes:
                    changes = self.get_changes(p)
                    if self.args.verbose:
                        print('{} changes for prefix {}'.format(
                            len(changes), p))
                    did_review += self.process_changes(changes)
                if did_review > 0:
                    self.update_stats()
                if not self.args.daemon:
                    break
                if self.args.verbose:
                    print('Finished! Going to sleep until next run')

            except (requests.exceptions.HTTPError, OSError) as e:
                sys.stderr.write('Error getting changes: ({})\n'.format(
                    str(e)))
                time.sleep(60)

            time.sleep(120)
예제 #2
0
class Troll(object):
    STRING_HEADER = '''
-- Automated message --
'''
    STRING_SUCCESS = '''
This change does not differ from its upstream source. It is certified {}
by review-o-matic!
'''
    STRING_INCORRECT_PREFIX = '''
This change has a BACKPORT prefix, however it does not differ from its upstream
source. The BACKPORT prefix should be primarily used for patches which were
altered during the cherry-pick (due to conflicts or downstream inconsistencies).

Consider changing your subject prefix to UPSTREAM (or FROMGIT/FROMLIST as
appropriate) to better reflect the contents of this patch.
'''
    STRING_MISSING_FIELDS = '''
Your commit message is missing the following required field(s):
    {}
'''
    STRING_MISSING_FIELDS_SUCCESS = '''
Don't worry, there is good news! Your patch does not differ from its upstream
source. Once the missing fields are present, it will be certified {} (or some
other similarly official-sounding certification) by review-o-matic!
'''
    STRING_MISSING_FIELDS_DIFF = '''
In addition to the missing fields, this patch differs from its upstream source.
This may be expected, this message is posted to make reviewing backports easier.
'''
    STRING_MISSING_HASH_HEADER = '''
Your commit message is missing the upstream commit hash. It should be in the
form:
'''
    STRING_MISSING_HASH_FMT_FROMGIT = '''
    (cherry picked from commit <commit SHA>
     <remote git url> <remote git tree>)
'''
    STRING_MISSING_HASH_FMT_UPSTREAM = '''
    (cherry picked from commit <commit SHA>)
'''
    STRING_MISSING_HASH_FOOTER = '''
Hint: Use the '-x' argument of git cherry-pick to add this automagically
'''
    STRING_MISSING_AM = '''
Your commit message is missing the patchwork URL. It should be in the
form:
    (am from https://patchwork.kernel.org/.../)
'''
    STRING_UNSUCCESSFUL_HEADER = '''
This patch differs from the source commit.

'''
    STRING_UPSTREAM_DIFF = '''
Since this is an UPSTREAM labeled patch, it shouldn't. Either this reviewing
script is incorrect (totally possible, pls send patches!), or something changed
when this was backported. If the backport required changes, please consider
using the BACKPORT label with a description of you downstream changes in your
commit message.
'''
    STRING_BACKPORT_DIFF = '''
This is expected, and this message is posted to make reviewing backports easier.
'''
    STRING_FROMGIT_DIFF = '''
This may be expected, this message is posted to make reviewing backports easier.
'''
    STRING_UNSUCCESSFUL_FOOTER = '''
Below is a diff of the upstream patch referenced in this commit message, vs this
patch.

'''
    STRING_FOUND_FIXES_REF = '''
!! NOTE: This patch has been referenced in the Fixes: tag of another commit. If
!!       you haven't already, consider backporting the following patch:
!!  {}
'''
    STRING_FOOTER = '''
---
To learn more about backporting kernel patches to Chromium OS, check out:
  https://chromium.googlesource.com/chromiumos/docs/+/master/kernel_faq.md#UPSTREAM_BACKPORT_FROMLIST_and-you

If you're curious about how this message was generated, head over to:
  https://github.com/atseanpaul/review-o-matic

This link is not useful:
  https://thats.poorly.run/
'''

    SWAG = [
        'Frrrresh', 'Crisper Than Cabbage', 'Awesome', 'Ahhhmazing',
        'Cool As A Cucumber', 'Most Excellent', 'Eximious', 'Prestantious',
        'Supernacular', 'Bodacious', 'Blue Chip', 'Blue Ribbon', 'Cracking',
        'Dandy', 'Dynamite', 'Fab', 'Fabulous', 'Fantabulous',
        'Scrumtrulescent', 'First Class', 'First Rate', 'First String',
        'Five Star', 'Gangbusters', 'Grand', 'Groovy', 'HYPE', 'Jim-Dandy',
        'Snazzy', 'Marvelous', 'Nifty', 'Par Excellence', 'Peachy Keen',
        'PHAT', 'Prime', 'Prizewinning', 'Quality', 'Radical', 'Righteous',
        'Sensational', 'Slick', 'Splendid', 'Lovely', 'Stellar', 'Sterling',
        'Superb', 'Superior', 'Superlative', 'Supernal', 'Swell', 'Terrific',
        'Tip-Top', 'Top Notch', 'Top Shelf', 'Unsurpassed', 'Wonderful'
    ]

    def __init__(self, url, args):
        self.url = url
        self.args = args
        self.gerrit = Gerrit(url)
        self.tag = 'autogenerated:review-o-matic'
        self.blacklist = []
        self.stats = {
            ReviewType.SUCCESS: 0,
            ReviewType.BACKPORT: 0,
            ReviewType.ALTERED_UPSTREAM: 0,
            ReviewType.MISSING_FIELDS: 0,
            ReviewType.MISSING_HASH: 0,
            ReviewType.INCORRECT_PREFIX: 0,
            ReviewType.FIXES_REF: 0
        }

    def do_review(self, review_type, change, fixes_ref, msg, notify, vote):
        final_msg = self.STRING_HEADER
        if fixes_ref:
            print('Adding fixes ref for change {}'.format(change.url()))
            self.stats[ReviewType.FIXES_REF] += 1
            final_msg += self.STRING_FOUND_FIXES_REF.format(fixes_ref)
        final_msg += msg
        final_msg += self.STRING_FOOTER

        self.stats[review_type] += 1
        if not self.args.dry_run:
            self.gerrit.review(change,
                               self.tag,
                               final_msg,
                               notify,
                               vote_code_review=vote)
        else:
            print('Review for change: {}'.format(change.url()))
            print('  Type:{}, Vote:{}, Notify:{}'.format(
                review_type, vote, notify))
            print(final_msg)
            print('------')

    def handle_successful_review(self, change, prefix, fixes_ref):
        # TODO: We should tag FROMLIST: BACKPORT: patches as incorrect, if needed
        if prefix == 'BACKPORT':
            print('Adding incorrect prefix review for change {}'.format(
                change.url()))
            msg = self.STRING_INCORRECT_PREFIX
            self.do_review(ReviewType.INCORRECT_PREFIX, change, fixes_ref, msg,
                           True, 0)
        else:
            print('Adding successful review for change {}'.format(
                change.url()))
            msg = self.STRING_SUCCESS.format(random.choice(self.SWAG))
            self.do_review(ReviewType.SUCCESS, change, fixes_ref, msg, True, 1)

    def handle_missing_fields_review(self, change, fields, result, fixes_ref):
        print('Adding missing fields review for change {}'.format(
            change.url()))
        missing = []
        if not fields['bug']:
            missing.append('BUG=')
        if not fields['test']:
            missing.append('TEST=')
        if not fields['sob']:
            cur_rev = change.current_revision
            missing.append('Signed-off-by: {} <{}>'.format(
                cur_rev.uploader_name, cur_rev.uploader_email))

        msg = self.STRING_MISSING_FIELDS.format(', '.join(missing))
        if len(result) == 0:
            msg += self.STRING_MISSING_FIELDS_SUCCESS.format(
                random.choice(self.SWAG))
        else:
            msg += self.STRING_MISSING_FIELDS_DIFF
            msg += self.STRING_UNSUCCESSFUL_FOOTER
            for l in result:
                msg += '{}\n'.format(l)

        self.do_review(ReviewType.MISSING_FIELDS, change, fixes_ref, msg, True,
                       -1)

    def handle_missing_hash_review(self, change, prefix):
        print('Adding missing hash review for change {}'.format(change.url()))
        msg = self.STRING_MISSING_HASH_HEADER
        if prefix == 'FROMGIT':
            msg += self.STRING_MISSING_HASH_FMT_FROMGIT
        else:
            msg += self.STRING_MISSING_HASH_FMT_UPSTREAM
        msg += self.STRING_MISSING_HASH_FOOTER
        self.do_review(ReviewType.MISSING_HASH, change, None, msg, True, -1)

    def handle_missing_am_review(self, change, prefix):
        print('Adding missing am URL for change {}'.format(change.url()))
        self.do_review(ReviewType.MISSING_HASH, change, None,
                       self.STRING_MISSING_AM, True, -1)

    def handle_unsuccessful_review(self, change, prefix, result, fixes_ref):
        vote = 0
        notify = False
        review_type = ReviewType.BACKPORT

        msg = self.STRING_UNSUCCESSFUL_HEADER
        if prefix == 'UPSTREAM':
            review_type = ReviewType.ALTERED_UPSTREAM
            vote = -1
            notify = True
            msg += self.STRING_UPSTREAM_DIFF
        elif prefix == 'BACKPORT':
            msg += self.STRING_BACKPORT_DIFF
        elif prefix == 'FROMGIT' or prefix == 'FROMLIST':
            msg += self.STRING_FROMGIT_DIFF

        msg += self.STRING_UNSUCCESSFUL_FOOTER

        for l in result:
            msg += '{}\n'.format(l)

        print('Adding unsuccessful review (vote={}) for change {}'.format(
            vote, change.url()))

        self.do_review(review_type, change, fixes_ref, msg, notify, vote)

    def get_changes(self, prefix):
        message = '{}:'.format(prefix)
        after = datetime.date.today() - datetime.timedelta(days=5)
        changes = self.gerrit.query_changes(
            status='open',
            message=message,
            after=after,
            project='chromiumos/third_party/kernel')
        return changes

    def print_error(self, error):
        if self.args.verbose:
            sys.stderr.write('\n')
        sys.stderr.write(error)

    def process_changes(self, prefix, changes):
        rev = Reviewer(git_dir=self.args.git_dir,
                       verbose=self.args.verbose,
                       chatty=self.args.chatty)
        num_changes = len(changes)
        cur_change = 1
        line_feed = False
        ret = False
        for c in changes:
            cur_rev = c.current_revision

            if self.args.chatty:
                print('Processing change {}'.format(c.url()))
            elif self.args.verbose:
                sys.stdout.write('{}Processing change {}/{}'.format(
                    '\r' if line_feed else '', cur_change, num_changes))
                cur_change += 1

            line_feed = True

            if c in self.blacklist:
                continue

            if not c.subject.startswith(prefix):
                continue

            skip = False
            for m in c.messages:
                if m.tag == self.tag and m.revision_num == cur_rev.number:
                    skip = True
            if skip and not self.args.force_cl:
                continue

            ret = True
            line_feed = False
            if self.args.verbose:
                print('')

            gerrit_patch = rev.get_commit_from_remote('cros', cur_rev.ref)

            if prefix == 'FROMLIST':
                upstream_patchworks = rev.get_am_from_from_patch(gerrit_patch)
                if not upstream_patchworks:
                    self.handle_missing_am_review(c, prefix)
                    continue

                upstream_patch = None
                for u in reversed(upstream_patchworks):
                    try:
                        upstream_patch = rev.get_commit_from_patchwork(u)
                        break
                    except:
                        continue

                if not upstream_patch:
                    self.print_error(
                        'ERROR: patch missing from patchwork, or patchwork host '
                        'not whitelisted for {} ({})\n'.format(
                            c, upstream_patchworks))
                    self.blacklist.append(c)
                    continue
            else:
                upstream_shas = rev.get_cherry_pick_shas_from_patch(
                    gerrit_patch)
                if not upstream_shas:
                    self.handle_missing_hash_review(c, prefix)
                    continue

                upstream_patch = None
                upstream_sha = None
                for s in reversed(upstream_shas):
                    try:
                        upstream_patch = rev.get_commit_from_sha(s)
                        upstream_sha = s
                        break
                    except:
                        continue

                if not upstream_patch:
                    self.print_error(
                        'ERROR: SHA missing from git for {} ({})\n'.format(
                            c, upstream_shas))
                    self.blacklist.append(c)
                    continue

            if prefix != 'FROMLIST':
                fixes_ref = rev.find_fixes_reference(upstream_sha)
            else:
                fixes_ref = None

            result = rev.compare_diffs(upstream_patch, gerrit_patch)

            fields = {'sob': False, 'bug': False, 'test': False}
            sob_re = re.compile('Signed-off-by:\s+{}'.format(
                cur_rev.uploader_name))
            for l in cur_rev.commit_message.splitlines():
                if l.startswith('BUG='):
                    fields['bug'] = True
                    continue
                if l.startswith('TEST='):
                    fields['test'] = True
                    continue
                if sob_re.match(l):
                    fields['sob'] = True
                    continue
            if not fields['bug'] or not fields['test'] or not fields['sob']:
                self.handle_missing_fields_review(c, fields, result, fixes_ref)
                continue

            if len(result) == 0:
                self.handle_successful_review(c, prefix, fixes_ref)
                continue

            self.handle_unsuccessful_review(c, prefix, result, fixes_ref)

        if self.args.verbose:
            print('')

        return ret

    def run(self):
        if self.args.force_cl != None:
            c = self.gerrit.get_change(self.args.force_cl)
            prefix = c.subject.split(':')[0]
            print('Force reviewing change  {}'.format(c))
            self.process_changes(prefix, [c])
            return

        while True:
            try:
                prefixes = ['UPSTREAM', 'BACKPORT', 'FROMGIT']
                did_review = False
                for p in prefixes:
                    changes = self.get_changes(p)
                    if self.args.verbose:
                        print('{} changes for prefix {}'.format(
                            len(changes), p))
                    did_review |= self.process_changes(p, changes)
                if did_review:
                    print('--')
                    summary = '  Summary: '
                    for k, v in self.stats.items():
                        summary += '{}={} '.format(k, v)
                    print(summary)
                    print('')
                if not self.args.daemon:
                    break
                if self.args.verbose:
                    print('Finished! Going to sleep until next run')

            except requests.exceptions.HTTPError as e:
                self.print_error('HTTPError ({})\n'.format(
                    e.response.status_code))
                time.sleep(60)

            time.sleep(120)
예제 #3
0
class Troll(object):
  RETRY_REVIEW_KEY='retry-bot-review'

  def __init__(self, config):
    self.config = config
    self.gerrit = Gerrit(config.gerrit_url, netrc=config.netrc)
    self.gerrit_admin = Gerrit(config.gerrit_url, netrc=config.netrc_admin)
    self.tag = 'autogenerated:review-o-matic'
    self.ignore_list = {}
    self.stats = TrollStats('{}'.format(self.config.stats_file))

  def do_review(self, project, change, review):
    logger.info('Review for change: {}'.format(change.url()))
    logger.info('  Issues: {}, Feedback: {}, Vote:{}, Notify:{}'.format(
        review.issues.keys(), review.feedback.keys(), review.vote,
        review.notify))

    if review.dry_run:
      print(review.generate_review_message(self.RETRY_REVIEW_KEY))
      if review.inline_comments:
        print('')
        print('-- Inline comments:')
        for f,comments in review.inline_comments.items():
          for c in comments:
            print('{}:{}'.format(f, c['line']))
            print(c['message'])

      print('------')
      return

    self.stats.update_for_review(project, review)

    self.gerrit.review(change, self.tag,
                       review.generate_review_message(self.RETRY_REVIEW_KEY),
                       review.notify, vote_code_review=review.vote,
                       inline_comments=review.inline_comments)

    if self.config.results_file:
      with open(self.config.results_file, 'a+') as f:
        f.write('{}: Issues: {}, Feedback: {}, Vote:{}, Notify:{}\n'.format(
          change.url(), review.issues.keys(), review.feedback.keys(),
          review.vote, review.notify))

  def get_changes(self, project, prefix):
    message = '{}:'.format(prefix)
    after = datetime.date.today() - datetime.timedelta(days=5)
    changes = self.gerrit.query_changes(status='open', message=message,
                    after=after, project=project.gerrit_project,
                    branches=project.monitor_branches)
    return changes

  def add_change_to_ignore_list(self, change):
    self.ignore_list[change.number] = change.current_revision.number

  def is_change_in_ignore_list(self, change):
    return self.ignore_list.get(change.number) == change.current_revision.number

  def process_change(self, project, rev, c):
    if self.config.chatty:
      logger.debug('Processing change {}'.format(c.url()))

    force_review = self.config.force_cl or self.config.force_all

    # Look for a retry request in the topic
    retry_request = False
    topic_list = c.topic.split() if c.topic else None
    if topic_list and self.RETRY_REVIEW_KEY in topic_list:
      retry_request = True
      force_review = True
      logger.error('Received retry request on change {} (topic={})'.format(
                   c.url(), c.topic))

    # Look for prior reviews and retry requests
    last_review = None
    for m in c.get_messages():
      if not m.revision_num == c.current_revision.number:
        continue
      if m.tag == self.tag:
        last_review = m

    age_days = None
    if not force_review and last_review:
      age_days = (datetime.datetime.utcnow() - last_review.date).days

    if age_days != None and self.config.chatty:
      logger.debug('    Reviewed {} days ago'.format(age_days))

    # Find a reviewer or ignore if not found
    reviewer = None
    if not ChangeReviewer.can_review_change(project, c, age_days):
      # Some patches are blanket unreviewable, check these first
      reviewer = None
    elif FromlistChangeReviewer.can_review_change(project, c, age_days):
      reviewer = FromlistChangeReviewer(project, rev, c,
                                        self.config.gerrit_msg_limit,
                                        self.config.dry_run)
    elif FromgitChangeReviewer.can_review_change(project, c, age_days):
      reviewer = FromgitChangeReviewer(project, rev, c,
                                       self.config.gerrit_msg_limit,
                                       self.config.dry_run, age_days)
    elif UpstreamChangeReviewer.can_review_change(project, c, age_days):
      reviewer = UpstreamChangeReviewer(project, rev, c,
                                        self.config.gerrit_msg_limit,
                                        self.config.dry_run)
    elif ChromiumChangeReviewer.can_review_change(project, c, age_days):
      reviewer = ChromiumChangeReviewer(project, rev, c,
                                        self.config.gerrit_msg_limit,
                                        self.config.dry_run,
                                        self.config.verbose)

    # Clear the retry request from the topic
    if retry_request:
      topic_list.remove(self.RETRY_REVIEW_KEY)
      c.topic = ' '.join(topic_list)
      if not self.gerrit_admin.set_topic(c):
        logger.error('ERROR: Failed to clear retry request from change')
        return None

    if not reviewer:
      self.add_change_to_ignore_list(c)
      return None

    if not force_review and self.is_change_in_ignore_list(c):
      return None

    return reviewer.review_patch()

  def process_changes(self, project, changes):
    rev = Reviewer(git_dir=project.local_repo, verbose=self.config.verbose,
                   chatty=self.config.chatty)
    ret = 0
    for c in changes:
      ignore = False
      for b in project.ignore_branches:
        if re.match(b, c.branch):
          ignore = True
          break
      if ignore:
        if self.config.chatty:
          logger.debug('Ignoring change {}'.format(c))
        self.add_change_to_ignore_list(c)
        continue

      try:
        result = self.process_change(project, rev, c)
        if result:
          self.do_review(project, c, result)
          ret += 1
        self.add_change_to_ignore_list(c)
      except GerritFetchError as e:
        logger.error('Gerrit fetch failed, will retry, {}'.format(c.url()))
        logger.exception('Exception: {}'.format(e))
        # Don't add change to ignore list, we want to retry next time
      except Exception as e:
        logger.error('Exception processing change {}'.format(c.url()))
        logger.exception('Exception: {}'.format(e))
        self.add_change_to_ignore_list(c)

    return ret

  def run(self):
    if self.config.force_cl:
      c = self.gerrit.get_change(self.config.force_cl, self.config.force_rev)
      logger.info('Force reviewing change  {}'.format(c))
      project = self.config.get_project(c.project)
      if not project:
        raise ValueError('Could not find project!')
      self.process_changes(project, [c])
      return

    while True:
      try:
        did_review = 0
        for project in self.config.projects.values():
          if (self.config.force_project and
              project.name != self.config.force_project):
            continue
          if self.config.chatty:
            logger.debug('Running for project {}'.format(project.name))
          for p in project.prefixes:
            changes = self.get_changes(project, p)
            if self.config.chatty:
              logger.debug('{} changes for prefix {}'.format(len(changes), p))
            did_review += self.process_changes(project, changes)

        if did_review > 0:
          self.stats.summarize(logging.INFO)
          if not self.config.dry_run:
            self.stats.save()

        if not self.config.daemon:
          return
        if self.config.chatty:
          logger.debug('Finished! Going to sleep until next run')

      except (requests.exceptions.HTTPError, OSError) as e:
        logger.error('Error getting changes: ({})'.format(str(e)))
        logger.exception('Exception getting changes: {}'.format(e))
        time.sleep(60)

      time.sleep(120)