def initialize_confs(conf): """Process configuration file""" config_file = os.path.expanduser(conf) # Allow inline comment with # confs_i = configparser.ConfigParser(inline_comment_prefixes=("#")) confs_i.read_string(config_template) confs_f = configparser.ConfigParser(inline_comment_prefixes=("#")) if os.path.exists(config_file): confs_f.read(config_file) if ("version" in confs_f["config"].keys() and confs_f["config"]["version"] == confs_i["config"]["version"]): confs = confs_f else: error_exit('''\ Error in {0}: version mismatch the current version: {1} the required version: {2} Rename {0} to {0}.bkup and make the new {0} by editing the template obtained by "imediff -t"'''.format( conf, confs_f["config"]["version"], confs_i["config"]["version"])) else: confs = confs_i return confs
def __init__( self, a=[], b=[], linerule=2, ): """ Construct a _LineMatcher """ # initialize self.a = a self.b = b if not (linerule >= 0 and linerule < 20): error_exit("E: linerule should be between 0 and 19 but {}".format( linerule)) # linerule: # 0 r"" -- drop none between text, but strip # 1 r"\s+" -- drop all whitespaces # 2 r"[\s\"']" -- drop all whitespaces and quotes (default) # 3 r"\W+" -- drop all non-alphanumerics # 10 r"" -- drop none between text, but strip and lowercase # 11 r"\s+" -- drop all whitespaces and lowercase # 12 r"[\s\"']" -- drop all whitespaces and quotes and lowercase # 13 r"\W+" -- drop all non-alphanumerics and lowercase if (linerule % 10) == 0: re_preform = re.compile(r"") elif (linerule % 10) == 1: re_preform = re.compile(r"\s+") elif (linerule % 10) == 2: re_preform = re.compile(r"[\s\"']+") elif (linerule % 10) == 3: re_preform = re.compile(r"\W+") else: re_preform = re.compile(r"") self.a_int = [] for ax in a: if linerule < 10: filtered_ax = re_preform.sub("", ax).strip() else: filtered_ax = re_preform.sub("", ax).strip().lower() self.a_int.append(filtered_ax) self.b_int = [] for bx in b: if linerule < 10: filtered_bx = re_preform.sub("", bx).strip() else: filtered_bx = re_preform.sub("", bx).strip().lower() self.b_int.append(filtered_bx) self.int = _LineMatcher(self.a_int, self.b_int, 0, len(self.a_int), 0, len(self.b_int))
def create_template(conf): config_file = os.path.expanduser(conf) if not os.path.exists(config_file): logger.debug("create configuration file: {}".format(conf)) try: with open(config_file, mode="w", buffering=io.DEFAULT_BUFFER_SIZE) as ofp: ofp.write(config_template) except IOError: error_exit("Error in creating configuration file: {}".format(conf)) else: error_exit("Erase {} before 'imediff -t'".format(conf)) return
def get_content(self, i): """Return content based on mode""" (tag, i1, i2, j1, j2, k1, k2, mode, row, bf) = self.opcodes[i] if tag == "E" or tag == "e": content = self.list_a[i1:i2] elif mode == "a": content = self.list_a[i1:i2] elif mode == "b": content = self.list_b[j1:j2] elif mode == "c": content = self.list_c[k1:k2] elif mode == "d": content = self.merge_diff(i) elif mode == "e": if bf is not None: content = bf else: error_exit("Bad mode='e' with missing edited buffer text\n") elif mode == "f": if self.diff_mode == 2: content = self.merge_wdiff2(i) else: # self.diff_mode == 3 (clean_merge, content) = self.merge_wdiff3(i) elif mode == "g": if self.diff_mode == 2: content = self.merge_diff(i) else: # self.diff_mode == 3 (clean_merge, content) = self.merge_wdiff3(i) else: error_exit("Bad mode='{}'\n".format(mode)) # content is at least [] (at least empty list) if content is None: error_exit("content can't be None") return content
def get_opcodes(self): if self.depth == 0: # depth = 0 side = 0 logger.debug( "=== a[{}:{}]/b[{}:{}] === d={:02d} === line[:] ===". format(self.is1, self.is2, self.js1, self.js2, self.depth)) elif self.depth > 0: # depth > 0 if self.depth % 2 == 1: # depth = 1, 3, 5, ... side = +1 logger.debug( "=== a[{}:{}]/b[{}:{}] === d={:02d} === line[:{:02d}] ===" .format(self.is1, self.is2, self.js1, self.js2, self.depth, self.length)) else: # self.depth % 2 == 0: # depth = 2, 4, 6, ... side = -1 logger.debug( "=== a[{}:{}]/b[{}:{}] === d={:02d} === line[-{:02d}:] ===" .format(self.is1, self.is2, self.js1, self.js2, self.depth, self.length)) else: error_exit( "=== a[{}:{}]/b[{}:{}] === d={:02d} should be non-negative". format(self.is1, self.is2, self.js1, self.js2, self.depth)) if side == 0: # self.is1, self.is2, self.js1, self.js2 are known to cover all am = self.a bm = self.b elif side == 1: # left side match am = [] bm = [] for i in range(self.is1, self.is2): am.append(self.a[i][:self.length]) for j in range(self.js1, self.js2): bm.append(self.b[j][:self.length]) else: # side == -1, right side match am = [] bm = [] for i in range(self.is1, self.is2): am.append(self.a[i][-self.length:]) for j in range(self.js1, self.js2): bm.append(self.b[j][-self.length:]) for i in range(self.is1, self.is2): logger.debug("filter a[{}] -> am[{}]='{}'".format( i, i - self.is1, am[i - self.is1])) for j in range(self.js1, self.js2): logger.debug("filter b[{}] -> bm[{}]='{}'".format( j, j - self.js1, bm[j - self.js1])) seq = SequenceMatcher(None, am, bm) match = [] for tag, i1, i2, j1, j2 in seq.get_opcodes(): logger.debug(">>> tag={} === a[{}:{}]/b[{}:{}]".format( tag, i1 + self.is1, i2 + self.is1, j1 + self.js1, j2 + self.js1)) if tag == "equal": for i in range(i1, i2): ip = self.is1 + i jp = self.js1 + j1 + i - i1 if side == 0: # full match on filtered line match.append(("E", ip, ip + 1, jp, jp + 1)) else: # partial match only (F for fuzzy) match.append(("F", ip, ip + 1, jp, jp + 1)) elif i1 == i2 or j1 == j2: # clean insert/delete match.append(("N", i1 + self.is1, i2 + self.is1, j1 + self.js1, j2 + self.js1)) elif (i1 + 1) == i2 and (j1 + 1) == j2: # single line change -> assume fuzzy match without checking match.append(("F", i1 + self.is1, i2 + self.is1, j1 + self.js1, j2 + self.js1)) else: # dig deeper for multi-line changes if side == 0: # full -> left side match.extend( _LineMatcher( a=self.a, b=self.b, is1=i1 + self.is1, is2=i2 + self.is1, js1=j1 + self.js1, js2=j2 + self.js1, length=self.length, depth=self.depth + 1, ).get_opcodes()) elif side == +1: # left side -> right side match.extend( _LineMatcher( a=self.a, b=self.b, is1=i1 + self.is1, is2=i2 + self.is1, js1=j1 + self.js1, js2=j2 + self.js1, length=self.length, depth=self.depth + 1, ).get_opcodes()) elif self.length > self.lenmin: # side == -1 # right side -> left side (shorter) match.extend( _LineMatcher( a=self.a, b=self.b, is1=i1 + self.is1, is2=i2 + self.is1, js1=j1 + self.js1, js2=j2 + self.js1, length=self.length * self.factor // 10, depth=self.depth + 1, ).get_opcodes()) else: # no more shorter, give up as multi-line block change match.append(( "N", i1 + self.is1, i2 + self.is1, j1 + self.js1, j2 + self.js1, )) logger.debug("<<< tag={} === a[{}:{}]/b[{}:{}]".format( tag, i1 + self.is1, i2 + self.is1, j1 + self.js1, j2 + self.js1)) return match
def main(): """ Entry point for imediff command Exit value 0 program exits normally after saving data 1 program quits without saving 2 program terminates after an internal error """ # preparation and arguments locale.setlocale(locale.LC_ALL, "") args = initialize_args() if args.template: create_template(args.conf) sys.exit(0) # logging logger.setLevel(logging.DEBUG) if args.debug: # create file handler which logs even debug messages fh = logging.FileHandler("imediff.log") else: # create file handler which doesn't log fh = logging.NullHandler() fh.setLevel(logging.DEBUG) # create console handler with a higher log level ch = logging.StreamHandler() ch.setLevel(logging.ERROR) # create formatter and add it to the handlers formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s") fh.setFormatter(formatter) ch.setFormatter(formatter) # add the handlers to the logger logger.addHandler(fh) logger.addHandler(ch) # configuration confs = initialize_confs(args.conf) editor = "editor" if "EDITOR" in os.environ: editor = os.environ["EDITOR"] if "editor" in confs["config"].keys(): editor = confs["config"]["editor"] args.edit_cmd = shutil.which(editor) if args.edit_cmd is None: args.edit_cmd = "/usr/bin/editor" # safe fall back logger.debug("external editor {} found as {}".format( editor, args.edit_cmd)) # normalize and process non-standard situation if args.version: print(_version) sys.exit(0) if args.diff_mode == 0 and args.non_interactive: tutorial = True if args.diff_mode == 0: args.diff_mode = 3 # Fake input list_a = (_opening + '\n Type "q" to quit this tutorial.').splitlines( keepends=True) list_b = [""] list_c = list_a confs["config"]["confirm_quit"] = "False" confs["config"]["confirm_exit"] = "False" tutorial = True elif args.diff_mode == 2: # diff2 list_a = read_lines(args.file_a) list_b = read_lines(args.file_b) list_c = None tutorial = False elif args.diff_mode == 3: list_a = read_lines(args.file_a) list_b = read_lines(args.file_b) list_c = read_lines(args.file_c) tutorial = False else: error_exit("imediff normally takes 2 or 3 files") # call main routine if not args.non_interactive: display_instance = TextPad(list_a, list_b, list_c, args, confs) display_instance.command_loop(tutorial=tutorial) del display_instance elif tutorial: print(_opening) else: text_instance = TextData(list_a, list_b, list_c, args, confs) text_instance.command_loop() del text_instance sys.exit(0)
def gui_loop(self, stdscr): # for curses TUI (core) # initialize self.stdscr = stdscr color = self.color # shorthand self.winh, self.winw = self.stdscr.getmaxyx() # window size curses.start_color() self.stdscr.clear() self.stdscr.refresh() # set color pair_number as (pair_number, fg, bg) curses.init_pair(1, cc[color["color_a"]], cc["BLACK"]) curses.init_pair(2, cc[color["color_b"]], cc["BLACK"]) curses.init_pair(3, cc[color["color_c"]], cc["BLACK"]) curses.init_pair(4, cc[color["color_d"]], cc["BLACK"]) curses.init_pair(5, cc[color["color_e"]], cc["BLACK"]) curses.init_pair(6, cc[color["color_f"]], cc["BLACK"]) # # +6: active cc self.active_color = 6 curses.init_pair(7, cc["WHITE"], cc[color["color_a"]]) curses.init_pair(8, cc["WHITE"], cc[color["color_b"]]) curses.init_pair(9, cc["WHITE"], cc[color["color_c"]]) curses.init_pair(10, cc["WHITE"], cc[color["color_d"]]) curses.init_pair(11, cc["WHITE"], cc[color["color_e"]]) curses.init_pair(12, cc["WHITE"], cc[color["color_f"]]) # # +12: deleted cc self.deleted_color = 12 curses.init_pair(13, cc[color["color_a"]], cc["WHITE"]) curses.init_pair(14, cc[color["color_b"]], cc["WHITE"]) curses.init_pair(15, cc[color["color_c"]], cc["WHITE"]) curses.init_pair(16, cc[color["color_d"]], cc["WHITE"]) curses.init_pair(17, cc[color["color_e"]], cc["WHITE"]) curses.init_pair(18, cc[color["color_f"]], cc["WHITE"]) # # +6+12 curses.init_pair(19, cc["BLACK"], cc[color["color_a"]]) curses.init_pair(20, cc["BLACK"], cc[color["color_b"]]) curses.init_pair(21, cc["BLACK"], cc[color["color_c"]]) curses.init_pair(22, cc["BLACK"], cc[color["color_d"]]) curses.init_pair(23, cc["BLACK"], cc[color["color_e"]]) curses.init_pair(24, cc["BLACK"], cc[color["color_f"]]) # if curses.has_colors() == False: self.mono = True if self.mono: self.mode = True self.color_a = "WHITE" self.color_b = "WHITE" self.color_c = "WHITE" self.color_d = "WHITE" self.color_e = "WHITE" self.color_f = "WHITE" else: if self.diff_mode == 2: self.color_a = color["color_a"] self.color_b = color["color_b"] # self.color_c = color['color_c'] # never used self.color_d = color["color_d"] self.color_e = color["color_e"] self.color_f = color["color_f"] else: self.color_a = color["color_a"] self.color_b = color["color_b"] self.color_c = color["color_c"] self.color_d = color["color_d"] self.color_e = color["color_e"] self.color_f = color["color_f"] # display parameters self.col = 0 # the column coordinate of textpad (left most=0) self.row = 0 # the row coordinate of textpad (top most=0) self.update_textpad = True # update textpad content while True: if self.active is not None: logger.debug( "command loop: active = {} active_index = {} row = {} col = {}" .format(self.active, self.actives[self.active], self.row, self.col)) else: logger.debug( "command loop: active = ***None*** row = {} col = {}". format(self.row, self.col)) curses.curs_set(0) if self.update_textpad: self.new_textpad() # clear to remove garbage outside of textpad self.winh, self.winw = self.stdscr.getmaxyx() self.adjust_window() for icol in range(self.contw - self.col, self.winw): self.stdscr.vline(0, icol, " ", self.winh) ##self.stdscr.vline(0, self.contw - self.col, '@', self.winh) ##self.stdscr.vline(0, self.winw-1, '*', self.winh) # clear rows downward to remove garbage characters for irow in range(self.conth - self.row, self.winh): self.stdscr.hline(irow, 0, " ", self.winw) ##if (self.conth - self.row) <= self.winh -1 and (self.conth - self.row) >= 0: ## self.stdscr.hline(self.conth - self.row , 0, '@', self.winw) if self.update_textpad or self.update_active: self.highlight() self.textpad.refresh(self.row, self.col, 0, 0, self.winh - 1, self.winw - 1) if self.active is not None: row = self.get_row(self.actives[self.active]) - self.row if row >= 0 and row < self.winh: self.stdscr.move(row, 0) curses.curs_set(1) else: curses.curs_set(0) self.stdscr.refresh() # reset flags self.update_textpad = False self.update_active = False if self.tutorial: c = ord("H") self.tutorial = False else: c = self.getch_translated() ch = chr(c) if ch == "w" or ch == "x" or c == curses.KEY_EXIT or c == curses.KEY_SAVE: if self.sloppy or (not self.sloppy and self.get_unresolved_count() == 0): if not self.confirm_exit or self.popup( _("Do you 'save and exit'? (Press '{y:c}' to exit)" ).format(y=self.rkc["y"])): output = self.get_output() write_file(self.file_o, output) break else: self.popup( _("Can't 'save and exit' due to the non-clean merge. (Press '{y:c}' to continue)" + "\n\n" + _nonclean).format(y=self.rkc["y"])) elif ch == "q": if not self.confirm_exit or self.popup( _("Do you 'quit without saving'? (Press '{y:c}' to quit)" ).format(y=self.rkc["y"])): self.opcodes = [] error_exit("Quit without saving by the user request\n") elif ch == "h" or c == curses.KEY_HELP: # Show help screen self.popup(self.helptext()) elif ch == "H": # Show tutorial screen self.popup(_tutorial) elif ch == "s" or ch == "?": # Show location if len(self.actives) == 0: self.popup( _stattext0.format(row=self.row, conth=self.conth, col=self.col)) else: self.popup( _stattext1.format( active=self.active, total=len(self.actives), unresolved=self.get_unresolved_count(), row=self.row, conth=self.conth, col=self.col, )) # Moves in document elif c == curses.KEY_SR or c == curses.KEY_UP or ch == "k": self.row -= 1 elif c == curses.KEY_SF or c == curses.KEY_DOWN or ch == "j": self.row += 1 elif c == curses.KEY_LEFT: self.col -= 8 elif c == curses.KEY_RIGHT: self.col += 8 elif c == curses.KEY_PPAGE: self.row -= self.winh elif c == curses.KEY_NPAGE: self.row += self.winh # Terminal resize signal elif c == curses.KEY_RESIZE: self.winh, self.winw = self.stdscr.getmaxyx() else: pass # Following key-command updates TextPad if self.active is not None: # get active chunk # Explicitly select chunk mode if ch in "abdef": self.set_mode(self.actives[self.active], ch) elif ch in "12456": self.set_mode(self.actives[self.active], chr(ord(ch) - ord("1") + ord("a"))) elif ch in "ABDEF": self.set_all_mode(ch.lower()) elif ch in "cg" and self.diff_mode == 3: self.set_mode(self.actives[self.active], ch) elif ch in "37" and self.diff_mode == 3: self.set_mode(self.actives[self.active], chr(ord(ch) - ord("1") + ord("a"))) elif ch in "CG" and self.diff_mode == 3: self.set_all_mode(ch.lower()) elif c == 10 or c == curses.KEY_COMMAND: mode = self.get_mode(self.actives[self.active]) if mode == "a": self.set_mode(self.actives[self.active], "b") elif mode == "b" and self.diff_mode == 2: self.set_mode(self.actives[self.active], "d") elif mode == "b" and self.diff_mode == 3: self.set_mode(self.actives[self.active], "c") elif mode == "c": self.set_mode(self.actives[self.active], "d") elif (mode == "d" and self.get_bf( self.actives[self.active]) is not None): self.set_mode(self.actives[self.active], "e") elif mode == "d": self.set_mode(self.actives[self.active], "f") elif mode == "e": self.set_mode(self.actives[self.active], "f") else: # f self.set_mode(self.actives[self.active], "a") elif ch == "m": self.editor(self.actives[self.active]) elif ch == "M" and mode == "e": self.del_editor(self.actives[self.active]) elif ch == "n" or c == curses.KEY_NEXT or ch == " ": self.active_next() elif ch == "p" or c == curses.KEY_PREVIOUS or c == curses.KEY_BACKSPACE: self.active_prev() elif ch == "t" or c == curses.KEY_HOME: self.active_home() elif ch == "z" or c == curses.KEY_END: self.active_end() elif ch == "N" or ch == "\t": self.diff_next() elif ch == "P" or c == curses.KEY_BTAB: self.diff_prev() elif ch == "T": self.diff_home() elif ch == "Z": self.diff_end() else: pass logger.debug("command-loop") return