Show More
This diff has been collapsed as it changes many lines, (569 lines changed) Show them Hide them | |||
@@ -183,7 +183,11 b' unexpectedly::' | |||
|
183 | 183 | |
|
184 | 184 | from __future__ import absolute_import |
|
185 | 185 | |
|
186 | import fcntl | |
|
187 | import functools | |
|
186 | 188 | import os |
|
189 | import struct | |
|
190 | import termios | |
|
187 | 191 | |
|
188 | 192 | from mercurial.i18n import _ |
|
189 | 193 | from mercurial import ( |
@@ -198,6 +202,7 b' from mercurial import (' | |||
|
198 | 202 | extensions, |
|
199 | 203 | hg, |
|
200 | 204 | lock, |
|
205 | logcmdutil, | |
|
201 | 206 | merge as mergemod, |
|
202 | 207 | mergeutil, |
|
203 | 208 | node, |
@@ -235,6 +240,9 b" configitem('histedit', 'linelen'," | |||
|
235 | 240 | configitem('histedit', 'singletransaction', |
|
236 | 241 | default=False, |
|
237 | 242 | ) |
|
243 | configitem('ui', 'interface.histedit', | |
|
244 | default=None, | |
|
245 | ) | |
|
238 | 246 | |
|
239 | 247 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
|
240 | 248 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
@@ -915,6 +923,562 b' def findoutgoing(ui, repo, remote=None, ' | |||
|
915 | 923 | raise error.Abort(msg, hint=hint) |
|
916 | 924 | return repo[roots[0]].node() |
|
917 | 925 | |
|
926 | # Curses Support | |
|
927 | try: | |
|
928 | import curses | |
|
929 | except ImportError: | |
|
930 | curses = None | |
|
931 | ||
|
932 | KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll'] | |
|
933 | ACTION_LABELS = { | |
|
934 | 'fold': '^fold', | |
|
935 | 'roll': '^roll', | |
|
936 | } | |
|
937 | ||
|
938 | COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN = 1, 2, 3, 4 | |
|
939 | ||
|
940 | E_QUIT, E_HISTEDIT = 1, 2 | |
|
941 | E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7 | |
|
942 | MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3 | |
|
943 | ||
|
944 | KEYTABLE = { | |
|
945 | 'global': { | |
|
946 | 'h': 'next-action', | |
|
947 | 'KEY_RIGHT': 'next-action', | |
|
948 | 'l': 'prev-action', | |
|
949 | 'KEY_LEFT': 'prev-action', | |
|
950 | 'q': 'quit', | |
|
951 | 'c': 'histedit', | |
|
952 | 'C': 'histedit', | |
|
953 | 'v': 'showpatch', | |
|
954 | '?': 'help', | |
|
955 | }, | |
|
956 | MODE_RULES: { | |
|
957 | 'd': 'action-drop', | |
|
958 | 'e': 'action-edit', | |
|
959 | 'f': 'action-fold', | |
|
960 | 'm': 'action-mess', | |
|
961 | 'p': 'action-pick', | |
|
962 | 'r': 'action-roll', | |
|
963 | ' ': 'select', | |
|
964 | 'j': 'down', | |
|
965 | 'k': 'up', | |
|
966 | 'KEY_DOWN': 'down', | |
|
967 | 'KEY_UP': 'up', | |
|
968 | 'J': 'move-down', | |
|
969 | 'K': 'move-up', | |
|
970 | 'KEY_NPAGE': 'move-down', | |
|
971 | 'KEY_PPAGE': 'move-up', | |
|
972 | '0': 'goto', # Used for 0..9 | |
|
973 | }, | |
|
974 | MODE_PATCH: { | |
|
975 | ' ': 'page-down', | |
|
976 | 'KEY_NPAGE': 'page-down', | |
|
977 | 'KEY_PPAGE': 'page-up', | |
|
978 | 'j': 'line-down', | |
|
979 | 'k': 'line-up', | |
|
980 | 'KEY_DOWN': 'line-down', | |
|
981 | 'KEY_UP': 'line-up', | |
|
982 | 'J': 'down', | |
|
983 | 'K': 'up', | |
|
984 | }, | |
|
985 | MODE_HELP: { | |
|
986 | }, | |
|
987 | } | |
|
988 | ||
|
989 | def screen_size(): | |
|
990 | return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' ')) | |
|
991 | ||
|
992 | class histeditrule(object): | |
|
993 | def __init__(self, ctx, pos, action='pick'): | |
|
994 | self.ctx = ctx | |
|
995 | self.action = action | |
|
996 | self.origpos = pos | |
|
997 | self.pos = pos | |
|
998 | self.conflicts = [] | |
|
999 | ||
|
1000 | def __str__(self): | |
|
1001 | # Some actions ('fold' and 'roll') combine a patch with a previous one. | |
|
1002 | # Add a marker showing which patch they apply to, and also omit the | |
|
1003 | # description for 'roll' (since it will get discarded). Example display: | |
|
1004 | # | |
|
1005 | # #10 pick 316392:06a16c25c053 add option to skip tests | |
|
1006 | # #11 ^roll 316393:71313c964cc5 | |
|
1007 | # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h | |
|
1008 | # #13 ^fold 316395:14ce5803f4c3 fix warnings | |
|
1009 | # | |
|
1010 | # The carets point to the changeset being folded into ("roll this | |
|
1011 | # changeset into the changeset above"). | |
|
1012 | action = ACTION_LABELS.get(self.action, self.action) | |
|
1013 | h = self.ctx.hex()[0:12] | |
|
1014 | r = self.ctx.rev() | |
|
1015 | desc = self.ctx.description().splitlines()[0].strip() | |
|
1016 | if self.action == 'roll': | |
|
1017 | desc = '' | |
|
1018 | return "#{0:<2} {1:<6} {2}:{3} {4}".format( | |
|
1019 | self.origpos, action, r, h, desc) | |
|
1020 | ||
|
1021 | def checkconflicts(self, other): | |
|
1022 | if other.pos > self.pos and other.origpos <= self.origpos: | |
|
1023 | if set(other.ctx.files()) & set(self.ctx.files()) != set(): | |
|
1024 | self.conflicts.append(other) | |
|
1025 | return self.conflicts | |
|
1026 | ||
|
1027 | if other in self.conflicts: | |
|
1028 | self.conflicts.remove(other) | |
|
1029 | return self.conflicts | |
|
1030 | ||
|
1031 | # ============ EVENTS =============== | |
|
1032 | def movecursor(state, oldpos, newpos): | |
|
1033 | '''Change the rule/changeset that the cursor is pointing to, regardless of | |
|
1034 | current mode (you can switch between patches from the view patch window).''' | |
|
1035 | state['pos'] = newpos | |
|
1036 | ||
|
1037 | mode, _ = state['mode'] | |
|
1038 | if mode == MODE_RULES: | |
|
1039 | # Scroll through the list by updating the view for MODE_RULES, so that | |
|
1040 | # even if we are not currently viewing the rules, switching back will | |
|
1041 | # result in the cursor's rule being visible. | |
|
1042 | modestate = state['modes'][MODE_RULES] | |
|
1043 | if newpos < modestate['line_offset']: | |
|
1044 | modestate['line_offset'] = newpos | |
|
1045 | elif newpos > modestate['line_offset'] + state['page_height'] - 1: | |
|
1046 | modestate['line_offset'] = newpos - state['page_height'] + 1 | |
|
1047 | ||
|
1048 | # Reset the patch view region to the top of the new patch. | |
|
1049 | state['modes'][MODE_PATCH]['line_offset'] = 0 | |
|
1050 | ||
|
1051 | def changemode(state, mode): | |
|
1052 | curmode, _ = state['mode'] | |
|
1053 | state['mode'] = (mode, curmode) | |
|
1054 | ||
|
1055 | def makeselection(state, pos): | |
|
1056 | state['selected'] = pos | |
|
1057 | ||
|
1058 | def swap(state, oldpos, newpos): | |
|
1059 | """Swap two positions and calculate necessary conflicts in | |
|
1060 | O(|newpos-oldpos|) time""" | |
|
1061 | ||
|
1062 | rules = state['rules'] | |
|
1063 | assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules) | |
|
1064 | ||
|
1065 | rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos] | |
|
1066 | ||
|
1067 | # TODO: swap should not know about histeditrule's internals | |
|
1068 | rules[newpos].pos = newpos | |
|
1069 | rules[oldpos].pos = oldpos | |
|
1070 | ||
|
1071 | start = min(oldpos, newpos) | |
|
1072 | end = max(oldpos, newpos) | |
|
1073 | for r in pycompat.xrange(start, end + 1): | |
|
1074 | rules[newpos].checkconflicts(rules[r]) | |
|
1075 | rules[oldpos].checkconflicts(rules[r]) | |
|
1076 | ||
|
1077 | if state['selected']: | |
|
1078 | makeselection(state, newpos) | |
|
1079 | ||
|
1080 | def changeaction(state, pos, action): | |
|
1081 | """Change the action state on the given position to the new action""" | |
|
1082 | rules = state['rules'] | |
|
1083 | assert 0 <= pos < len(rules) | |
|
1084 | rules[pos].action = action | |
|
1085 | ||
|
1086 | def cycleaction(state, pos, next=False): | |
|
1087 | """Changes the action state the next or the previous action from | |
|
1088 | the action list""" | |
|
1089 | rules = state['rules'] | |
|
1090 | assert 0 <= pos < len(rules) | |
|
1091 | current = rules[pos].action | |
|
1092 | ||
|
1093 | assert current in KEY_LIST | |
|
1094 | ||
|
1095 | index = KEY_LIST.index(current) | |
|
1096 | if next: | |
|
1097 | index += 1 | |
|
1098 | else: | |
|
1099 | index -= 1 | |
|
1100 | changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)]) | |
|
1101 | ||
|
1102 | def changeview(state, delta, unit): | |
|
1103 | '''Change the region of whatever is being viewed (a patch or the list of | |
|
1104 | changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.''' | |
|
1105 | mode, _ = state['mode'] | |
|
1106 | if mode != MODE_PATCH: | |
|
1107 | return | |
|
1108 | mode_state = state['modes'][mode] | |
|
1109 | num_lines = len(patchcontents(state)) | |
|
1110 | page_height = state['page_height'] | |
|
1111 | unit = page_height if unit == 'page' else 1 | |
|
1112 | num_pages = 1 + (num_lines - 1) / page_height | |
|
1113 | max_offset = (num_pages - 1) * page_height | |
|
1114 | newline = mode_state['line_offset'] + delta * unit | |
|
1115 | mode_state['line_offset'] = max(0, min(max_offset, newline)) | |
|
1116 | ||
|
1117 | def event(state, ch): | |
|
1118 | """Change state based on the current character input | |
|
1119 | ||
|
1120 | This takes the current state and based on the current character input from | |
|
1121 | the user we change the state. | |
|
1122 | """ | |
|
1123 | selected = state['selected'] | |
|
1124 | oldpos = state['pos'] | |
|
1125 | rules = state['rules'] | |
|
1126 | ||
|
1127 | if ch in (curses.KEY_RESIZE, "KEY_RESIZE"): | |
|
1128 | return E_RESIZE | |
|
1129 | ||
|
1130 | lookup_ch = ch | |
|
1131 | if '0' <= ch <= '9': | |
|
1132 | lookup_ch = '0' | |
|
1133 | ||
|
1134 | curmode, prevmode = state['mode'] | |
|
1135 | action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch)) | |
|
1136 | if action is None: | |
|
1137 | return | |
|
1138 | if action in ('down', 'move-down'): | |
|
1139 | newpos = min(oldpos + 1, len(rules) - 1) | |
|
1140 | movecursor(state, oldpos, newpos) | |
|
1141 | if selected is not None or action == 'move-down': | |
|
1142 | swap(state, oldpos, newpos) | |
|
1143 | elif action in ('up', 'move-up'): | |
|
1144 | newpos = max(0, oldpos - 1) | |
|
1145 | movecursor(state, oldpos, newpos) | |
|
1146 | if selected is not None or action == 'move-up': | |
|
1147 | swap(state, oldpos, newpos) | |
|
1148 | elif action == 'next-action': | |
|
1149 | cycleaction(state, oldpos, next=True) | |
|
1150 | elif action == 'prev-action': | |
|
1151 | cycleaction(state, oldpos, next=False) | |
|
1152 | elif action == 'select': | |
|
1153 | selected = oldpos if selected is None else None | |
|
1154 | makeselection(state, selected) | |
|
1155 | elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10: | |
|
1156 | newrule = next((r for r in rules if r.origpos == int(ch))) | |
|
1157 | movecursor(state, oldpos, newrule.pos) | |
|
1158 | if selected is not None: | |
|
1159 | swap(state, oldpos, newrule.pos) | |
|
1160 | elif action.startswith('action-'): | |
|
1161 | changeaction(state, oldpos, action[7:]) | |
|
1162 | elif action == 'showpatch': | |
|
1163 | changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode) | |
|
1164 | elif action == 'help': | |
|
1165 | changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode) | |
|
1166 | elif action == 'quit': | |
|
1167 | return E_QUIT | |
|
1168 | elif action == 'histedit': | |
|
1169 | return E_HISTEDIT | |
|
1170 | elif action == 'page-down': | |
|
1171 | return E_PAGEDOWN | |
|
1172 | elif action == 'page-up': | |
|
1173 | return E_PAGEUP | |
|
1174 | elif action == 'line-down': | |
|
1175 | return E_LINEDOWN | |
|
1176 | elif action == 'line-up': | |
|
1177 | return E_LINEUP | |
|
1178 | ||
|
1179 | def makecommands(rules): | |
|
1180 | """Returns a list of commands consumable by histedit --commands based on | |
|
1181 | our list of rules""" | |
|
1182 | commands = [] | |
|
1183 | for rules in rules: | |
|
1184 | commands.append("{0} {1}\n".format(rules.action, rules.ctx)) | |
|
1185 | return commands | |
|
1186 | ||
|
1187 | def addln(win, y, x, line, color=None): | |
|
1188 | """Add a line to the given window left padding but 100% filled with | |
|
1189 | whitespace characters, so that the color appears on the whole line""" | |
|
1190 | maxy, maxx = win.getmaxyx() | |
|
1191 | length = maxx - 1 - x | |
|
1192 | line = ("{0:<%d}" % length).format(str(line).strip())[:length] | |
|
1193 | if y < 0: | |
|
1194 | y = maxy + y | |
|
1195 | if x < 0: | |
|
1196 | x = maxx + x | |
|
1197 | if color: | |
|
1198 | win.addstr(y, x, line, color) | |
|
1199 | else: | |
|
1200 | win.addstr(y, x, line) | |
|
1201 | ||
|
1202 | def patchcontents(state): | |
|
1203 | repo = state['repo'] | |
|
1204 | rule = state['rules'][state['pos']] | |
|
1205 | displayer = logcmdutil.changesetdisplayer(repo.ui, repo, { | |
|
1206 | 'patch': True, 'verbose': True | |
|
1207 | }, buffered=True) | |
|
1208 | displayer.show(rule.ctx) | |
|
1209 | displayer.close() | |
|
1210 | return displayer.hunk[rule.ctx.rev()].splitlines() | |
|
1211 | ||
|
1212 | def _chisteditmain(repo, rules, stdscr): | |
|
1213 | # initialize color pattern | |
|
1214 | curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE) | |
|
1215 | curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE) | |
|
1216 | curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW) | |
|
1217 | curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN) | |
|
1218 | ||
|
1219 | # don't display the cursor | |
|
1220 | try: | |
|
1221 | curses.curs_set(0) | |
|
1222 | except curses.error: | |
|
1223 | pass | |
|
1224 | ||
|
1225 | def rendercommit(win, state): | |
|
1226 | """Renders the commit window that shows the log of the current selected | |
|
1227 | commit""" | |
|
1228 | pos = state['pos'] | |
|
1229 | rules = state['rules'] | |
|
1230 | rule = rules[pos] | |
|
1231 | ||
|
1232 | ctx = rule.ctx | |
|
1233 | win.box() | |
|
1234 | ||
|
1235 | maxy, maxx = win.getmaxyx() | |
|
1236 | length = maxx - 3 | |
|
1237 | ||
|
1238 | line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx) | |
|
1239 | win.addstr(1, 1, line[:length]) | |
|
1240 | ||
|
1241 | line = "user: {0}".format(stringutil.shortuser(ctx.user())) | |
|
1242 | win.addstr(2, 1, line[:length]) | |
|
1243 | ||
|
1244 | bms = repo.nodebookmarks(ctx.node()) | |
|
1245 | line = "bookmark: {0}".format(' '.join(bms)) | |
|
1246 | win.addstr(3, 1, line[:length]) | |
|
1247 | ||
|
1248 | line = "files: {0}".format(','.join(ctx.files())) | |
|
1249 | win.addstr(4, 1, line[:length]) | |
|
1250 | ||
|
1251 | line = "summary: {0}".format(ctx.description().splitlines()[0]) | |
|
1252 | win.addstr(5, 1, line[:length]) | |
|
1253 | ||
|
1254 | conflicts = rule.conflicts | |
|
1255 | if len(conflicts) > 0: | |
|
1256 | conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts)) | |
|
1257 | conflictstr = "changed files overlap with {0}".format(conflictstr) | |
|
1258 | else: | |
|
1259 | conflictstr = 'no overlap' | |
|
1260 | ||
|
1261 | win.addstr(6, 1, conflictstr[:length]) | |
|
1262 | win.noutrefresh() | |
|
1263 | ||
|
1264 | def helplines(mode): | |
|
1265 | if mode == MODE_PATCH: | |
|
1266 | help = """\ | |
|
1267 | ?: help, k/up: line up, j/down: line down, v: stop viewing patch | |
|
1268 | pgup: prev page, space/pgdn: next page, c: commit, q: abort | |
|
1269 | """ | |
|
1270 | else: | |
|
1271 | help = """\ | |
|
1272 | ?: help, k/up: move up, j/down: move down, space: select, v: view patch | |
|
1273 | d: drop, e: edit, f: fold, m: mess, p: pick, r: roll | |
|
1274 | pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort | |
|
1275 | """ | |
|
1276 | return help.splitlines() | |
|
1277 | ||
|
1278 | def renderhelp(win, state): | |
|
1279 | maxy, maxx = win.getmaxyx() | |
|
1280 | mode, _ = state['mode'] | |
|
1281 | for y, line in enumerate(helplines(mode)): | |
|
1282 | if y >= maxy: | |
|
1283 | break | |
|
1284 | addln(win, y, 0, line, curses.color_pair(COLOR_HELP)) | |
|
1285 | win.noutrefresh() | |
|
1286 | ||
|
1287 | def renderrules(rulesscr, state): | |
|
1288 | rules = state['rules'] | |
|
1289 | pos = state['pos'] | |
|
1290 | selected = state['selected'] | |
|
1291 | start = state['modes'][MODE_RULES]['line_offset'] | |
|
1292 | ||
|
1293 | conflicts = [r.ctx for r in rules if r.conflicts] | |
|
1294 | if len(conflicts) > 0: | |
|
1295 | line = "potential conflict in %s" % ','.join(map(str, conflicts)) | |
|
1296 | addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN)) | |
|
1297 | ||
|
1298 | for y, rule in enumerate(rules[start:]): | |
|
1299 | if y >= state['page_height']: | |
|
1300 | break | |
|
1301 | if len(rule.conflicts) > 0: | |
|
1302 | rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN)) | |
|
1303 | else: | |
|
1304 | rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK) | |
|
1305 | if y + start == selected: | |
|
1306 | addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED)) | |
|
1307 | elif y + start == pos: | |
|
1308 | addln(rulesscr, y, 2, rule, curses.A_BOLD) | |
|
1309 | else: | |
|
1310 | addln(rulesscr, y, 2, rule) | |
|
1311 | rulesscr.noutrefresh() | |
|
1312 | ||
|
1313 | def renderstring(win, state, output): | |
|
1314 | maxy, maxx = win.getmaxyx() | |
|
1315 | length = min(maxy - 1, len(output)) | |
|
1316 | for y in range(0, length): | |
|
1317 | win.addstr(y, 0, output[y]) | |
|
1318 | win.noutrefresh() | |
|
1319 | ||
|
1320 | def renderpatch(win, state): | |
|
1321 | start = state['modes'][MODE_PATCH]['line_offset'] | |
|
1322 | renderstring(win, state, patchcontents(state)[start:]) | |
|
1323 | ||
|
1324 | def layout(mode): | |
|
1325 | maxy, maxx = stdscr.getmaxyx() | |
|
1326 | helplen = len(helplines(mode)) | |
|
1327 | return { | |
|
1328 | 'commit': (8, maxx), | |
|
1329 | 'help': (helplen, maxx), | |
|
1330 | 'main': (maxy - helplen - 8, maxx), | |
|
1331 | } | |
|
1332 | ||
|
1333 | def drawvertwin(size, y, x): | |
|
1334 | win = curses.newwin(size[0], size[1], y, x) | |
|
1335 | y += size[0] | |
|
1336 | return win, y, x | |
|
1337 | ||
|
1338 | state = { | |
|
1339 | 'pos': 0, | |
|
1340 | 'rules': rules, | |
|
1341 | 'selected': None, | |
|
1342 | 'mode': (MODE_INIT, MODE_INIT), | |
|
1343 | 'page_height': None, | |
|
1344 | 'modes': { | |
|
1345 | MODE_RULES: { | |
|
1346 | 'line_offset': 0, | |
|
1347 | }, | |
|
1348 | MODE_PATCH: { | |
|
1349 | 'line_offset': 0, | |
|
1350 | } | |
|
1351 | }, | |
|
1352 | 'repo': repo, | |
|
1353 | } | |
|
1354 | ||
|
1355 | # eventloop | |
|
1356 | ch = None | |
|
1357 | stdscr.clear() | |
|
1358 | stdscr.refresh() | |
|
1359 | while True: | |
|
1360 | try: | |
|
1361 | oldmode, _ = state['mode'] | |
|
1362 | if oldmode == MODE_INIT: | |
|
1363 | changemode(state, MODE_RULES) | |
|
1364 | e = event(state, ch) | |
|
1365 | ||
|
1366 | if e == E_QUIT: | |
|
1367 | return False | |
|
1368 | if e == E_HISTEDIT: | |
|
1369 | return state['rules'] | |
|
1370 | else: | |
|
1371 | if e == E_RESIZE: | |
|
1372 | size = screen_size() | |
|
1373 | if size != stdscr.getmaxyx(): | |
|
1374 | curses.resizeterm(*size) | |
|
1375 | ||
|
1376 | curmode, _ = state['mode'] | |
|
1377 | sizes = layout(curmode) | |
|
1378 | if curmode != oldmode: | |
|
1379 | state['page_height'] = sizes['main'][0] | |
|
1380 | # Adjust the view to fit the current screen size. | |
|
1381 | movecursor(state, state['pos'], state['pos']) | |
|
1382 | ||
|
1383 | # Pack the windows against the top, each pane spread across the | |
|
1384 | # full width of the screen. | |
|
1385 | y, x = (0, 0) | |
|
1386 | helpwin, y, x = drawvertwin(sizes['help'], y, x) | |
|
1387 | mainwin, y, x = drawvertwin(sizes['main'], y, x) | |
|
1388 | commitwin, y, x = drawvertwin(sizes['commit'], y, x) | |
|
1389 | ||
|
1390 | if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP): | |
|
1391 | if e == E_PAGEDOWN: | |
|
1392 | changeview(state, +1, 'page') | |
|
1393 | elif e == E_PAGEUP: | |
|
1394 | changeview(state, -1, 'page') | |
|
1395 | elif e == E_LINEDOWN: | |
|
1396 | changeview(state, +1, 'line') | |
|
1397 | elif e == E_LINEUP: | |
|
1398 | changeview(state, -1, 'line') | |
|
1399 | ||
|
1400 | # start rendering | |
|
1401 | commitwin.erase() | |
|
1402 | helpwin.erase() | |
|
1403 | mainwin.erase() | |
|
1404 | if curmode == MODE_PATCH: | |
|
1405 | renderpatch(mainwin, state) | |
|
1406 | elif curmode == MODE_HELP: | |
|
1407 | renderstring(mainwin, state, __doc__.strip().splitlines()) | |
|
1408 | else: | |
|
1409 | renderrules(mainwin, state) | |
|
1410 | rendercommit(commitwin, state) | |
|
1411 | renderhelp(helpwin, state) | |
|
1412 | curses.doupdate() | |
|
1413 | # done rendering | |
|
1414 | ch = stdscr.getkey() | |
|
1415 | except curses.error: | |
|
1416 | pass | |
|
1417 | ||
|
1418 | def _chistedit(ui, repo, *freeargs, **opts): | |
|
1419 | """interactively edit changeset history via a curses interface | |
|
1420 | ||
|
1421 | Provides a ncurses interface to histedit. Press ? in chistedit mode | |
|
1422 | to see an extensive help. Requires python-curses to be installed.""" | |
|
1423 | ||
|
1424 | if curses is None: | |
|
1425 | raise error.Abort(_("Python curses library required")) | |
|
1426 | ||
|
1427 | # disable color | |
|
1428 | ui._colormode = None | |
|
1429 | ||
|
1430 | try: | |
|
1431 | keep = opts.get('keep') | |
|
1432 | revs = opts.get('rev', [])[:] | |
|
1433 | cmdutil.checkunfinished(repo) | |
|
1434 | cmdutil.bailifchanged(repo) | |
|
1435 | ||
|
1436 | if os.path.exists(os.path.join(repo.path, 'histedit-state')): | |
|
1437 | raise error.Abort(_('history edit already in progress, try ' | |
|
1438 | '--continue or --abort')) | |
|
1439 | revs.extend(freeargs) | |
|
1440 | if not revs: | |
|
1441 | defaultrev = destutil.desthistedit(ui, repo) | |
|
1442 | if defaultrev is not None: | |
|
1443 | revs.append(defaultrev) | |
|
1444 | if len(revs) != 1: | |
|
1445 | raise error.Abort( | |
|
1446 | _('histedit requires exactly one ancestor revision')) | |
|
1447 | ||
|
1448 | rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs))) | |
|
1449 | if len(rr) != 1: | |
|
1450 | raise error.Abort(_('The specified revisions must have ' | |
|
1451 | 'exactly one common root')) | |
|
1452 | root = rr[0].node() | |
|
1453 | ||
|
1454 | topmost, empty = repo.dirstate.parents() | |
|
1455 | revs = between(repo, root, topmost, keep) | |
|
1456 | if not revs: | |
|
1457 | raise error.Abort(_('%s is not an ancestor of working directory') % | |
|
1458 | node.short(root)) | |
|
1459 | ||
|
1460 | ctxs = [] | |
|
1461 | for i, r in enumerate(revs): | |
|
1462 | ctxs.append(histeditrule(repo[r], i)) | |
|
1463 | rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs)) | |
|
1464 | curses.echo() | |
|
1465 | curses.endwin() | |
|
1466 | if rc is False: | |
|
1467 | ui.write(_("chistedit aborted\n")) | |
|
1468 | return 0 | |
|
1469 | if type(rc) is list: | |
|
1470 | ui.status(_("running histedit\n")) | |
|
1471 | rules = makecommands(rc) | |
|
1472 | filename = repo.vfs.join('chistedit') | |
|
1473 | with open(filename, 'w+') as fp: | |
|
1474 | for r in rules: | |
|
1475 | fp.write(r) | |
|
1476 | opts['commands'] = filename | |
|
1477 | return _texthistedit(ui, repo, *freeargs, **opts) | |
|
1478 | except KeyboardInterrupt: | |
|
1479 | pass | |
|
1480 | return -1 | |
|
1481 | ||
|
918 | 1482 | @command('histedit', |
|
919 | 1483 | [('', 'commands', '', |
|
920 | 1484 | _('read history edits from the specified file'), _('FILE')), |
@@ -1029,6 +1593,11 b' def histedit(ui, repo, *freeargs, **opts' | |||
|
1029 | 1593 | for intentional "edit" command, but also for resolving unexpected |
|
1030 | 1594 | conflicts). |
|
1031 | 1595 | """ |
|
1596 | if ui.interface('histedit') == 'curses': | |
|
1597 | return _chistedit(ui, repo, *freeargs, **opts) | |
|
1598 | return _texthistedit(ui, repo, *freeargs, **opts) | |
|
1599 | ||
|
1600 | def _texthistedit(ui, repo, *freeargs, **opts): | |
|
1032 | 1601 | state = histeditstate(repo) |
|
1033 | 1602 | try: |
|
1034 | 1603 | state.wlock = repo.wlock() |
General Comments 0
You need to be logged in to leave comments.
Login now