#!/usr/bin/python import os import sys from pprint import pprint from collections import defaultdict print 'using newsmerge' result = [] # get the file names from argv. The order depends on the hgrc configuration, # this assumes newsmerge.args = $base $local $other -o $output base = sys.argv[1] local = sys.argv[2] other = sys.argv[3] output = sys.argv[5] print base, local, other, output # possible states used by parse_news empty_line = 0 file_header = 1 section_header = 2 news_entry = 3 def parse_news(fname): """ Parse a Misc/NEWS file and return a list of [section, list_of_news] for the latest/topmost Python release. """ # this is a simple state machine to parse a Misc/NEWS files and extract # the news entries grouped by section status = empty_line sections = [] with open(fname) as f: prev_line = '' for full_line in f: line = full_line.strip() # check for a line of +, i.e. main title of the file (ignored) if set(line) == set('+'): if status == empty_line: status = file_header else: status = empty_line elif status == empty_line: if not line: continue # if the line starts with '- ', it's a news entry if line.startswith('- '): status = news_entry news = [full_line] # otherwise the title of a new section else: status = section_header elif status == section_header: # check for a line of -, i.e. section (Library, Tests, ...) if set(line) == set('-'): section_news = [] sections.append([prev_line, section_news]) # check for a line of =, i.e. the header for a specific Python # version. If *sections* is not empty, we got to the header # of an older Python version and we should stop parsing elif set(line) == set('=') and sections: break status = empty_line elif status == news_entry: # if we are parsing a news entry, either keep adding lines if line: news.append(line) # or if the entry is over, add it to its section else: section_news.append(news) status = empty_line prev_line = line #pprint(sections) return sections # parse the 3 files: # base = common ancestor (e.g. previous 3.4 changeset) # local = working copy we want to modify (e.g. default) # other = files that we already modified (e.g. cs we just made on 3.4) base_content = parse_news(base) local_content = parse_news(local) other_content = parse_news(other) base_sections = [section for section, news in base_content] local_sections = [section for section, news in local_content] other_sections = [section for section, news in other_content if section in base_sections] # make a diff between base and other, to find what news entries have been # added in the previous changeset (other). Add those to new_entries and later # write them to local. Additional entries that might already be in local # (e.g. in case of heads merge on the same branch) won't be affected. base_content_dict = dict(base_content) other_content_dict = dict(other_content) new_entries = defaultdict(list) for section in other_sections: base_section = base_content_dict[section] other_section = other_content_dict[section] for entry in other_section: if entry not in base_section: # get all the entries that are in "other" but not in "base" # (i.e. the ones that have been added) new_entries[section].append(entry) # now read all the content from local and copy it to Misc/NEWS, inserting # the news in *new_entries* at the top of each section #print new_entries with open(local) as fr, open(output, 'w') as fw: entries_buffer = [] for full_line in fr: fw.write(full_line) if not new_entries and not entries_buffer: continue # all the news have been added already line = full_line.strip() # check if we reached a section where entries should be added if line and line in new_entries: print '*** found', line # assume all the sections already exist in local entries_buffer = new_entries.pop(line) # if we have news entries in the buffer we are at the top of a section # so insert here all the entries of the buffer before proceeding elif not line and entries_buffer: print '*** writing:', entries_buffer for entry in entries_buffer: fw.writelines(entry) fw.write(full_line) entries_buffer = []