1# Minimal support for git commands on an hg repository 2# 3# Copyright 2005, 2006 Chris Mason <mason@suse.com> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8'''browse the repository in a graphical way 9 10The hgk extension allows browsing the history of a repository in a 11graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is not 12distributed with Mercurial.) 13 14hgk consists of two parts: a Tcl script that does the displaying and 15querying of information, and an extension to Mercurial named hgk.py, 16which provides hooks for hgk to get information. hgk can be found in 17the contrib directory, and the extension is shipped in the hgext 18repository, and needs to be enabled. 19 20The :hg:`view` command will launch the hgk Tcl script. For this command 21to work, hgk must be in your search path. Alternately, you can specify 22the path to hgk in your configuration file:: 23 24 [hgk] 25 path = /location/of/hgk 26 27hgk can make use of the extdiff extension to visualize revisions. 28Assuming you had already configured extdiff vdiff command, just add:: 29 30 [hgk] 31 vdiff=vdiff 32 33Revisions context menu will now display additional entries to fire 34vdiff on hovered and selected revisions. 35''' 36 37from __future__ import absolute_import 38 39import os 40 41from mercurial.i18n import _ 42from mercurial.node import ( 43 nullrev, 44 short, 45) 46from mercurial import ( 47 commands, 48 obsolete, 49 patch, 50 pycompat, 51 registrar, 52 scmutil, 53) 54 55cmdtable = {} 56command = registrar.command(cmdtable) 57# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for 58# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should 59# be specifying the version(s) of Mercurial they are tested with, or 60# leave the attribute unspecified. 61testedwith = b'ships-with-hg-core' 62 63configtable = {} 64configitem = registrar.configitem(configtable) 65 66configitem( 67 b'hgk', 68 b'path', 69 default=b'hgk', 70) 71 72 73@command( 74 b'debug-diff-tree', 75 [ 76 (b'p', b'patch', None, _(b'generate patch')), 77 (b'r', b'recursive', None, _(b'recursive')), 78 (b'P', b'pretty', None, _(b'pretty')), 79 (b's', b'stdin', None, _(b'stdin')), 80 (b'C', b'copy', None, _(b'detect copies')), 81 (b'S', b'search', b"", _(b'search')), 82 ], 83 b'[OPTION]... NODE1 NODE2 [FILE]...', 84 inferrepo=True, 85) 86def difftree(ui, repo, node1=None, node2=None, *files, **opts): 87 """diff trees from two commits""" 88 89 def __difftree(repo, node1, node2, files=None): 90 assert node2 is not None 91 if files is None: 92 files = [] 93 mmap = repo[node1].manifest() 94 mmap2 = repo[node2].manifest() 95 m = scmutil.match(repo[node1], files) 96 st = repo.status(node1, node2, m) 97 empty = short(repo.nullid) 98 99 for f in st.modified: 100 # TODO get file permissions 101 ui.writenoi18n( 102 b":100664 100664 %s %s M\t%s\t%s\n" 103 % (short(mmap[f]), short(mmap2[f]), f, f) 104 ) 105 for f in st.added: 106 ui.writenoi18n( 107 b":000000 100664 %s %s N\t%s\t%s\n" 108 % (empty, short(mmap2[f]), f, f) 109 ) 110 for f in st.removed: 111 ui.writenoi18n( 112 b":100664 000000 %s %s D\t%s\t%s\n" 113 % (short(mmap[f]), empty, f, f) 114 ) 115 116 ## 117 118 while True: 119 if opts['stdin']: 120 line = ui.fin.readline() 121 if not line: 122 break 123 line = line.rstrip(pycompat.oslinesep).split(b' ') 124 node1 = line[0] 125 if len(line) > 1: 126 node2 = line[1] 127 else: 128 node2 = None 129 node1 = repo.lookup(node1) 130 if node2: 131 node2 = repo.lookup(node2) 132 else: 133 node2 = node1 134 node1 = repo.changelog.parents(node1)[0] 135 if opts['patch']: 136 if opts['pretty']: 137 catcommit(ui, repo, node2, b"") 138 m = scmutil.match(repo[node1], files) 139 diffopts = patch.difffeatureopts(ui) 140 diffopts.git = True 141 chunks = patch.diff(repo, node1, node2, match=m, opts=diffopts) 142 for chunk in chunks: 143 ui.write(chunk) 144 else: 145 __difftree(repo, node1, node2, files=files) 146 if not opts['stdin']: 147 break 148 149 150def catcommit(ui, repo, n, prefix, ctx=None): 151 nlprefix = b'\n' + prefix 152 if ctx is None: 153 ctx = repo[n] 154 # use ctx.node() instead ?? 155 ui.write((b"tree %s\n" % short(ctx.changeset()[0]))) 156 for p in ctx.parents(): 157 ui.write((b"parent %s\n" % p)) 158 159 date = ctx.date() 160 description = ctx.description().replace(b"\0", b"") 161 ui.write((b"author %s %d %d\n" % (ctx.user(), int(date[0]), date[1]))) 162 163 if b'committer' in ctx.extra(): 164 ui.write((b"committer %s\n" % ctx.extra()[b'committer'])) 165 166 ui.write((b"revision %d\n" % ctx.rev())) 167 ui.write((b"branch %s\n" % ctx.branch())) 168 if obsolete.isenabled(repo, obsolete.createmarkersopt): 169 if ctx.obsolete(): 170 ui.writenoi18n(b"obsolete\n") 171 ui.write((b"phase %s\n\n" % ctx.phasestr())) 172 173 if prefix != b"": 174 ui.write( 175 b"%s%s\n" % (prefix, description.replace(b'\n', nlprefix).strip()) 176 ) 177 else: 178 ui.write(description + b"\n") 179 if prefix: 180 ui.write(b'\0') 181 182 183@command(b'debug-merge-base', [], _(b'REV REV')) 184def base(ui, repo, node1, node2): 185 """output common ancestor information""" 186 node1 = repo.lookup(node1) 187 node2 = repo.lookup(node2) 188 n = repo.changelog.ancestor(node1, node2) 189 ui.write(short(n) + b"\n") 190 191 192@command( 193 b'debug-cat-file', 194 [(b's', b'stdin', None, _(b'stdin'))], 195 _(b'[OPTION]... TYPE FILE'), 196 inferrepo=True, 197) 198def catfile(ui, repo, type=None, r=None, **opts): 199 """cat a specific revision""" 200 # in stdin mode, every line except the commit is prefixed with two 201 # spaces. This way the our caller can find the commit without magic 202 # strings 203 # 204 prefix = b"" 205 if opts['stdin']: 206 line = ui.fin.readline() 207 if not line: 208 return 209 (type, r) = line.rstrip(pycompat.oslinesep).split(b' ') 210 prefix = b" " 211 else: 212 if not type or not r: 213 ui.warn(_(b"cat-file: type or revision not supplied\n")) 214 commands.help_(ui, b'cat-file') 215 216 while r: 217 if type != b"commit": 218 ui.warn(_(b"aborting hg cat-file only understands commits\n")) 219 return 1 220 n = repo.lookup(r) 221 catcommit(ui, repo, n, prefix) 222 if opts['stdin']: 223 line = ui.fin.readline() 224 if not line: 225 break 226 (type, r) = line.rstrip(pycompat.oslinesep).split(b' ') 227 else: 228 break 229 230 231# git rev-tree is a confusing thing. You can supply a number of 232# commit sha1s on the command line, and it walks the commit history 233# telling you which commits are reachable from the supplied ones via 234# a bitmask based on arg position. 235# you can specify a commit to stop at by starting the sha1 with ^ 236def revtree(ui, args, repo, full=b"tree", maxnr=0, parents=False): 237 def chlogwalk(): 238 count = len(repo) 239 i = count 240 l = [0] * 100 241 chunk = 100 242 while True: 243 if chunk > i: 244 chunk = i 245 i = 0 246 else: 247 i -= chunk 248 249 for x in pycompat.xrange(chunk): 250 if i + x >= count: 251 l[chunk - x :] = [0] * (chunk - x) 252 break 253 if full is not None: 254 if (i + x) in repo: 255 l[x] = repo[i + x] 256 l[x].changeset() # force reading 257 else: 258 if (i + x) in repo: 259 l[x] = 1 260 for x in pycompat.xrange(chunk - 1, -1, -1): 261 if l[x] != 0: 262 yield (i + x, full is not None and l[x] or None) 263 if i == 0: 264 break 265 266 # calculate and return the reachability bitmask for sha 267 def is_reachable(ar, reachable, sha): 268 if len(ar) == 0: 269 return 1 270 mask = 0 271 for i in pycompat.xrange(len(ar)): 272 if sha in reachable[i]: 273 mask |= 1 << i 274 275 return mask 276 277 reachable = [] 278 stop_sha1 = [] 279 want_sha1 = [] 280 count = 0 281 282 # figure out which commits they are asking for and which ones they 283 # want us to stop on 284 for i, arg in enumerate(args): 285 if arg.startswith(b'^'): 286 s = repo.lookup(arg[1:]) 287 stop_sha1.append(s) 288 want_sha1.append(s) 289 elif arg != b'HEAD': 290 want_sha1.append(repo.lookup(arg)) 291 292 # calculate the graph for the supplied commits 293 for i, n in enumerate(want_sha1): 294 reachable.append(set()) 295 visit = [n] 296 reachable[i].add(n) 297 while visit: 298 n = visit.pop(0) 299 if n in stop_sha1: 300 continue 301 for p in repo.changelog.parents(n): 302 if p not in reachable[i]: 303 reachable[i].add(p) 304 visit.append(p) 305 if p in stop_sha1: 306 continue 307 308 # walk the repository looking for commits that are in our 309 # reachability graph 310 for i, ctx in chlogwalk(): 311 if i not in repo: 312 continue 313 n = repo.changelog.node(i) 314 mask = is_reachable(want_sha1, reachable, n) 315 if mask: 316 parentstr = b"" 317 if parents: 318 pp = repo.changelog.parents(n) 319 if pp[0] != repo.nullid: 320 parentstr += b" " + short(pp[0]) 321 if pp[1] != repo.nullid: 322 parentstr += b" " + short(pp[1]) 323 if not full: 324 ui.write(b"%s%s\n" % (short(n), parentstr)) 325 elif full == b"commit": 326 ui.write(b"%s%s\n" % (short(n), parentstr)) 327 catcommit(ui, repo, n, b' ', ctx) 328 else: 329 (p1, p2) = repo.changelog.parents(n) 330 (h, h1, h2) = map(short, (n, p1, p2)) 331 (i1, i2) = map(repo.changelog.rev, (p1, p2)) 332 333 date = ctx.date()[0] 334 ui.write(b"%s %s:%s" % (date, h, mask)) 335 mask = is_reachable(want_sha1, reachable, p1) 336 if i1 != nullrev and mask > 0: 337 ui.write(b"%s:%s " % (h1, mask)), 338 mask = is_reachable(want_sha1, reachable, p2) 339 if i2 != nullrev and mask > 0: 340 ui.write(b"%s:%s " % (h2, mask)) 341 ui.write(b"\n") 342 if maxnr and count >= maxnr: 343 break 344 count += 1 345 346 347# git rev-list tries to order things by date, and has the ability to stop 348# at a given commit without walking the whole repo. TODO add the stop 349# parameter 350@command( 351 b'debug-rev-list', 352 [ 353 (b'H', b'header', None, _(b'header')), 354 (b't', b'topo-order', None, _(b'topo-order')), 355 (b'p', b'parents', None, _(b'parents')), 356 (b'n', b'max-count', 0, _(b'max-count')), 357 ], 358 b'[OPTION]... REV...', 359) 360def revlist(ui, repo, *revs, **opts): 361 """print revisions""" 362 if opts['header']: 363 full = b"commit" 364 else: 365 full = None 366 copy = [x for x in revs] 367 revtree(ui, copy, repo, full, opts['max_count'], opts[r'parents']) 368 369 370@command( 371 b'view', 372 [(b'l', b'limit', b'', _(b'limit number of changes displayed'), _(b'NUM'))], 373 _(b'[-l LIMIT] [REVRANGE]'), 374 helpcategory=command.CATEGORY_CHANGE_NAVIGATION, 375) 376def view(ui, repo, *etc, **opts): 377 """start interactive history viewer""" 378 opts = pycompat.byteskwargs(opts) 379 os.chdir(repo.root) 380 optstr = b' '.join( 381 [b'--%s %s' % (k, v) for k, v in pycompat.iteritems(opts) if v] 382 ) 383 if repo.filtername is None: 384 optstr += b'--hidden' 385 386 cmd = ui.config(b"hgk", b"path") + b" %s %s" % (optstr, b" ".join(etc)) 387 ui.debug(b"running %s\n" % cmd) 388 ui.system(cmd, blockedtag=b'hgk_view') 389