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 | from __future__ import absolute_import |
|
184 | from __future__ import absolute_import | |
185 |
|
185 | |||
|
186 | import fcntl | |||
|
187 | import functools | |||
186 | import os |
|
188 | import os | |
|
189 | import struct | |||
|
190 | import termios | |||
187 |
|
191 | |||
188 | from mercurial.i18n import _ |
|
192 | from mercurial.i18n import _ | |
189 | from mercurial import ( |
|
193 | from mercurial import ( | |
@@ -198,6 +202,7 b' from mercurial import (' | |||||
198 | extensions, |
|
202 | extensions, | |
199 | hg, |
|
203 | hg, | |
200 | lock, |
|
204 | lock, | |
|
205 | logcmdutil, | |||
201 | merge as mergemod, |
|
206 | merge as mergemod, | |
202 | mergeutil, |
|
207 | mergeutil, | |
203 | node, |
|
208 | node, | |
@@ -235,6 +240,9 b" configitem('histedit', 'linelen'," | |||||
235 | configitem('histedit', 'singletransaction', |
|
240 | configitem('histedit', 'singletransaction', | |
236 | default=False, |
|
241 | default=False, | |
237 | ) |
|
242 | ) | |
|
243 | configitem('ui', 'interface.histedit', | |||
|
244 | default=None, | |||
|
245 | ) | |||
238 |
|
246 | |||
239 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
|
247 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | |
240 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
|
248 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | |
@@ -915,6 +923,562 b' def findoutgoing(ui, repo, remote=None, ' | |||||
915 | raise error.Abort(msg, hint=hint) |
|
923 | raise error.Abort(msg, hint=hint) | |
916 | return repo[roots[0]].node() |
|
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 | @command('histedit', |
|
1482 | @command('histedit', | |
919 | [('', 'commands', '', |
|
1483 | [('', 'commands', '', | |
920 | _('read history edits from the specified file'), _('FILE')), |
|
1484 | _('read history edits from the specified file'), _('FILE')), | |
@@ -1029,6 +1593,11 b' def histedit(ui, repo, *freeargs, **opts' | |||||
1029 | for intentional "edit" command, but also for resolving unexpected |
|
1593 | for intentional "edit" command, but also for resolving unexpected | |
1030 | conflicts). |
|
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 | state = histeditstate(repo) |
|
1601 | state = histeditstate(repo) | |
1033 | try: |
|
1602 | try: | |
1034 | state.wlock = repo.wlock() |
|
1603 | state.wlock = repo.wlock() |
@@ -1245,7 +1245,11 b' class ui(object):' | |||||
1245 | "chunkselector": [ |
|
1245 | "chunkselector": [ | |
1246 | "text", |
|
1246 | "text", | |
1247 | "curses", |
|
1247 | "curses", | |
1248 | ] |
|
1248 | ], | |
|
1249 | "histedit": [ | |||
|
1250 | "text", | |||
|
1251 | "curses", | |||
|
1252 | ], | |||
1249 | } |
|
1253 | } | |
1250 |
|
1254 | |||
1251 | # Feature-specific interface |
|
1255 | # Feature-specific interface |
General Comments 0
You need to be logged in to leave comments.
Login now