##// END OF EJS Templates
Remove trailing spaces, fix indentation
Thomas Arendsen Hein -
r5143:d4fa6baf default
parent child Browse files
Show More
@@ -1,3758 +1,3758 b''
1 1 #!/usr/bin/env wish
2 2
3 3 # Copyright (C) 2005 Paul Mackerras. All rights reserved.
4 4 # This program is free software; it may be used, copied, modified
5 5 # and distributed under the terms of the GNU General Public Licence,
6 6 # either version 2, or (at your option) any later version.
7 7
8 8
9 9 # Modified version of Tip 171:
10 10 # http://www.tcl.tk/cgi-bin/tct/tip/171.html
11 11 #
12 12 # The in_mousewheel global was added to fix strange reentrancy issues.
13 13 # The whole snipped is activated only under windows, mouse wheel
14 14 # bindings working already under MacOSX and Linux.
15 15
16 16 if {[tk windowingsystem] eq "win32"} {
17 17
18 18 set mw_classes [list Text Listbox Table TreeCtrl]
19 19 foreach class $mw_classes { bind $class <MouseWheel> {} }
20 20
21 21 set in_mousewheel 0
22 22
23 23 proc ::tk::MouseWheel {wFired X Y D {shifted 0}} {
24 24 global in_mousewheel
25 25 if { $in_mousewheel != 0 } { return }
26 26 # Set event to check based on call
27 27 set evt "<[expr {$shifted?{Shift-}:{}}]MouseWheel>"
28 28 # do not double-fire in case the class already has a binding
29 29 if {[bind [winfo class $wFired] $evt] ne ""} { return }
30 30 # obtain the window the mouse is over
31 31 set w [winfo containing $X $Y]
32 32 # if we are outside the app, try and scroll the focus widget
33 33 if {![winfo exists $w]} { catch {set w [focus]} }
34 34 if {[winfo exists $w]} {
35
35
36 36 if {[bind $w $evt] ne ""} {
37 37 # Awkward ... this widget has a MouseWheel binding, but to
38 38 # trigger successfully in it, we must give it focus.
39 39 catch {focus} old
40 40 if {$w ne $old} { focus $w }
41 41 set in_mousewheel 1
42 42 event generate $w $evt -rootx $X -rooty $Y -delta $D
43 43 set in_mousewheel 0
44 44 if {$w ne $old} { focus $old }
45 45 return
46 46 }
47 47
48 48 # aqua and x11/win32 have different delta handling
49 49 if {[tk windowingsystem] ne "aqua"} {
50 50 set delta [expr {- ($D / 30)}]
51 51 } else {
52 52 set delta [expr {- ($D)}]
53 53 }
54 54 # scrollbars have different call conventions
55 55 if {[string match "*Scrollbar" [winfo class $w]]} {
56 56 catch {tk::ScrollByUnits $w \
57 57 [string index [$w cget -orient] 0] $delta}
58 58 } else {
59 59 set cmd [list $w [expr {$shifted ? "xview" : "yview"}] \
60 60 scroll $delta units]
61 61 # Walking up to find the proper widget (handles cases like
62 62 # embedded widgets in a canvas)
63 63 while {[catch $cmd] && [winfo toplevel $w] ne $w} {
64 64 set w [winfo parent $w]
65 65 }
66 66 }
67 67 }
68 68 }
69 69
70 70 bind all <MouseWheel> [list ::tk::MouseWheel %W %X %Y %D 0]
71 71
72 72 # end of win32 section
73 }
73 }
74 74
75 75
76 76 proc gitdir {} {
77 77 global env
78 78 if {[info exists env(GIT_DIR)]} {
79 79 return $env(GIT_DIR)
80 80 } else {
81 81 return ".hg"
82 82 }
83 83 }
84 84
85 85 proc getcommits {rargs} {
86 86 global commits commfd phase canv mainfont env
87 87 global startmsecs nextupdate ncmupdate
88 88 global ctext maincursor textcursor leftover
89 89
90 90 # check that we can find a .git directory somewhere...
91 91 set gitdir [gitdir]
92 92 if {![file isdirectory $gitdir]} {
93 93 error_popup "Cannot find the git directory \"$gitdir\"."
94 94 exit 1
95 95 }
96 96 set commits {}
97 97 set phase getcommits
98 98 set startmsecs [clock clicks -milliseconds]
99 99 set nextupdate [expr $startmsecs + 100]
100 100 set ncmupdate 1
101 101 set limit 0
102 102 set revargs {}
103 103 for {set i 0} {$i < [llength $rargs]} {incr i} {
104 104 set opt [lindex $rargs $i]
105 105 if {$opt == "--limit"} {
106 106 incr i
107 107 set limit [lindex $rargs $i]
108 108 } else {
109 109 lappend revargs $opt
110 110 }
111 111 }
112 112 if [catch {
113 113 set parse_args [concat --default HEAD $revargs]
114 114 set parse_temp [eval exec {$env(HG)} --config ui.report_untrusted=false debug-rev-parse $parse_args]
115 115 regsub -all "\r\n" $parse_temp "\n" parse_temp
116 116 set parsed_args [split $parse_temp "\n"]
117 117 } err] {
118 118 # if git-rev-parse failed for some reason...
119 119 if {$rargs == {}} {
120 120 set revargs HEAD
121 121 }
122 122 set parsed_args $revargs
123 123 }
124 124 if {$limit > 0} {
125 125 set parsed_args [concat -n $limit $parsed_args]
126 126 }
127 127 if [catch {
128 128 set commfd [open "|{$env(HG)} --config ui.report_untrusted=false debug-rev-list --header --topo-order --parents $parsed_args" r]
129 129 } err] {
130 130 puts stderr "Error executing hg debug-rev-list: $err"
131 131 exit 1
132 132 }
133 133 set leftover {}
134 134 fconfigure $commfd -blocking 0 -translation lf
135 135 fileevent $commfd readable [list getcommitlines $commfd]
136 136 $canv delete all
137 137 $canv create text 3 3 -anchor nw -text "Reading commits..." \
138 138 -font $mainfont -tags textitems
139 139 . config -cursor watch
140 140 settextcursor watch
141 141 }
142 142
143 143 proc getcommitlines {commfd} {
144 144 global commits parents cdate children
145 145 global commitlisted phase commitinfo nextupdate
146 146 global stopped redisplaying leftover
147 147
148 148 set stuff [read $commfd]
149 149 if {$stuff == {}} {
150 150 if {![eof $commfd]} return
151 151 # set it blocking so we wait for the process to terminate
152 152 fconfigure $commfd -blocking 1
153 153 if {![catch {close $commfd} err]} {
154 154 after idle finishcommits
155 155 return
156 156 }
157 157 if {[string range $err 0 4] == "usage"} {
158 158 set err \
159 159 {Gitk: error reading commits: bad arguments to git-rev-list.
160 160 (Note: arguments to gitk are passed to git-rev-list
161 161 to allow selection of commits to be displayed.)}
162 162 } else {
163 163 set err "Error reading commits: $err"
164 164 }
165 165 error_popup $err
166 166 exit 1
167 167 }
168 168 set start 0
169 169 while 1 {
170 170 set i [string first "\0" $stuff $start]
171 171 if {$i < 0} {
172 172 append leftover [string range $stuff $start end]
173 173 return
174 174 }
175 175 set cmit [string range $stuff $start [expr {$i - 1}]]
176 176 if {$start == 0} {
177 177 set cmit "$leftover$cmit"
178 178 set leftover {}
179 179 }
180 180 set start [expr {$i + 1}]
181 181 regsub -all "\r\n" $cmit "\n" cmit
182 182 set j [string first "\n" $cmit]
183 183 set ok 0
184 184 if {$j >= 0} {
185 185 set ids [string range $cmit 0 [expr {$j - 1}]]
186 186 set ok 1
187 187 foreach id $ids {
188 188 if {![regexp {^[0-9a-f]{12}$} $id]} {
189 189 set ok 0
190 190 break
191 191 }
192 192 }
193 193 }
194 194 if {!$ok} {
195 195 set shortcmit $cmit
196 196 if {[string length $shortcmit] > 80} {
197 197 set shortcmit "[string range $shortcmit 0 80]..."
198 198 }
199 199 error_popup "Can't parse hg debug-rev-list output: {$shortcmit}"
200 200 exit 1
201 201 }
202 202 set id [lindex $ids 0]
203 203 set olds [lrange $ids 1 end]
204 204 set cmit [string range $cmit [expr {$j + 1}] end]
205 205 lappend commits $id
206 206 set commitlisted($id) 1
207 207 parsecommit $id $cmit 1 [lrange $ids 1 end]
208 208 drawcommit $id
209 209 if {[clock clicks -milliseconds] >= $nextupdate} {
210 210 doupdate 1
211 211 }
212 212 while {$redisplaying} {
213 213 set redisplaying 0
214 214 if {$stopped == 1} {
215 215 set stopped 0
216 216 set phase "getcommits"
217 217 foreach id $commits {
218 218 drawcommit $id
219 219 if {$stopped} break
220 220 if {[clock clicks -milliseconds] >= $nextupdate} {
221 221 doupdate 1
222 222 }
223 223 }
224 224 }
225 225 }
226 226 }
227 227 }
228 228
229 229 proc doupdate {reading} {
230 230 global commfd nextupdate numcommits ncmupdate
231 231
232 232 if {$reading} {
233 233 fileevent $commfd readable {}
234 234 }
235 235 update
236 236 set nextupdate [expr {[clock clicks -milliseconds] + 100}]
237 237 if {$numcommits < 100} {
238 238 set ncmupdate [expr {$numcommits + 1}]
239 239 } elseif {$numcommits < 10000} {
240 240 set ncmupdate [expr {$numcommits + 10}]
241 241 } else {
242 242 set ncmupdate [expr {$numcommits + 100}]
243 243 }
244 244 if {$reading} {
245 245 fileevent $commfd readable [list getcommitlines $commfd]
246 246 }
247 247 }
248 248
249 249 proc readcommit {id} {
250 250 global env
251 251 if [catch {set contents [exec $env(HG) --config ui.report_untrusted=false debug-cat-file commit $id]}] return
252 252 parsecommit $id $contents 0 {}
253 253 }
254 254
255 255 proc parsecommit {id contents listed olds} {
256 256 global commitinfo children nchildren parents nparents cdate ncleft
257 257
258 258 set inhdr 1
259 259 set comment {}
260 260 set headline {}
261 261 set auname {}
262 262 set audate {}
263 263 set comname {}
264 264 set comdate {}
265 265 set rev {}
266 266 if {![info exists nchildren($id)]} {
267 267 set children($id) {}
268 268 set nchildren($id) 0
269 269 set ncleft($id) 0
270 270 }
271 271 set parents($id) $olds
272 272 set nparents($id) [llength $olds]
273 273 foreach p $olds {
274 274 if {![info exists nchildren($p)]} {
275 275 set children($p) [list $id]
276 276 set nchildren($p) 1
277 277 set ncleft($p) 1
278 278 } elseif {[lsearch -exact $children($p) $id] < 0} {
279 279 lappend children($p) $id
280 280 incr nchildren($p)
281 281 incr ncleft($p)
282 282 }
283 283 }
284 284 regsub -all "\r\n" $contents "\n" contents
285 285 foreach line [split $contents "\n"] {
286 286 if {$inhdr} {
287 287 set line [split $line]
288 288 if {$line == {}} {
289 289 set inhdr 0
290 290 } else {
291 291 set tag [lindex $line 0]
292 292 if {$tag == "author"} {
293 293 set x [expr {[llength $line] - 2}]
294 294 set audate [lindex $line $x]
295 295 set auname [join [lrange $line 1 [expr {$x - 1}]]]
296 296 } elseif {$tag == "committer"} {
297 297 set x [expr {[llength $line] - 2}]
298 298 set comdate [lindex $line $x]
299 299 set comname [join [lrange $line 1 [expr {$x - 1}]]]
300 300 } elseif {$tag == "revision"} {
301 301 set rev [lindex $line 1]
302 302 }
303 303 }
304 304 } else {
305 305 if {$comment == {}} {
306 306 set headline [string trim $line]
307 307 } else {
308 308 append comment "\n"
309 309 }
310 310 if {!$listed} {
311 311 # git-rev-list indents the comment by 4 spaces;
312 312 # if we got this via git-cat-file, add the indentation
313 313 append comment " "
314 314 }
315 315 append comment $line
316 316 }
317 317 }
318 318 if {$audate != {}} {
319 319 set audate [clock format $audate -format "%Y-%m-%d %H:%M:%S"]
320 320 }
321 321 if {$comdate != {}} {
322 322 set cdate($id) $comdate
323 323 set comdate [clock format $comdate -format "%Y-%m-%d %H:%M:%S"]
324 324 }
325 325 set commitinfo($id) [list $headline $auname $audate \
326 326 $comname $comdate $comment $rev]
327 327 }
328 328
329 329 proc readrefs {} {
330 330 global tagids idtags headids idheads tagcontents env
331 331
332 332 set tags [exec $env(HG) --config ui.report_untrusted=false tags]
333 333 regsub -all "\r\n" $tags "\n" tags
334 334 set lines [split $tags "\n"]
335 335 foreach f $lines {
336 336 regexp {(\S+)$} $f full
337 337 regsub {\s+(\S+)$} $f "" direct
338 338 set sha [split $full ':']
339 339 set tag [lindex $sha 1]
340 340 lappend tagids($direct) $tag
341 341 lappend idtags($tag) $direct
342 342 }
343 343 }
344 344
345 345 proc readotherrefs {base dname excl} {
346 346 global otherrefids idotherrefs
347 347
348 348 set git [gitdir]
349 349 set files [glob -nocomplain -types f [file join $git $base *]]
350 350 foreach f $files {
351 351 catch {
352 352 set fd [open $f r]
353 353 set line [read $fd 40]
354 354 if {[regexp {^[0-9a-f]{12}} $line id]} {
355 355 set name "$dname[file tail $f]"
356 356 set otherrefids($name) $id
357 357 lappend idotherrefs($id) $name
358 358 }
359 359 close $fd
360 360 }
361 361 }
362 362 set dirs [glob -nocomplain -types d [file join $git $base *]]
363 363 foreach d $dirs {
364 364 set dir [file tail $d]
365 365 if {[lsearch -exact $excl $dir] >= 0} continue
366 366 readotherrefs [file join $base $dir] "$dname$dir/" {}
367 367 }
368 368 }
369 369
370 370 proc allcansmousewheel {delta} {
371 371 set delta [expr -5*(int($delta)/abs($delta))]
372 372 allcanvs yview scroll $delta units
373 373 }
374 374
375 375 proc error_popup msg {
376 376 set w .error
377 377 toplevel $w
378 378 wm transient $w .
379 379 message $w.m -text $msg -justify center -aspect 400
380 380 pack $w.m -side top -fill x -padx 20 -pady 20
381 381 button $w.ok -text OK -command "destroy $w"
382 382 pack $w.ok -side bottom -fill x
383 383 bind $w <Visibility> "grab $w; focus $w"
384 384 tkwait window $w
385 385 }
386 386
387 387 proc makewindow {} {
388 388 global canv canv2 canv3 linespc charspc ctext cflist textfont
389 389 global findtype findtypemenu findloc findstring fstring geometry
390 390 global entries sha1entry sha1string sha1but
391 391 global maincursor textcursor curtextcursor
392 392 global rowctxmenu gaudydiff mergemax
393 393
394 394 menu .bar
395 395 .bar add cascade -label "File" -menu .bar.file
396 396 menu .bar.file
397 397 .bar.file add command -label "Reread references" -command rereadrefs
398 398 .bar.file add command -label "Quit" -command doquit
399 399 menu .bar.help
400 400 .bar add cascade -label "Help" -menu .bar.help
401 401 .bar.help add command -label "About gitk" -command about
402 402 . configure -menu .bar
403 403
404 404 if {![info exists geometry(canv1)]} {
405 405 set geometry(canv1) [expr 45 * $charspc]
406 406 set geometry(canv2) [expr 30 * $charspc]
407 407 set geometry(canv3) [expr 15 * $charspc]
408 408 set geometry(canvh) [expr 25 * $linespc + 4]
409 409 set geometry(ctextw) 80
410 410 set geometry(ctexth) 30
411 411 set geometry(cflistw) 30
412 412 }
413 413 panedwindow .ctop -orient vertical
414 414 if {[info exists geometry(width)]} {
415 415 .ctop conf -width $geometry(width) -height $geometry(height)
416 416 set texth [expr {$geometry(height) - $geometry(canvh) - 56}]
417 417 set geometry(ctexth) [expr {($texth - 8) /
418 418 [font metrics $textfont -linespace]}]
419 419 }
420 420 frame .ctop.top
421 421 frame .ctop.top.bar
422 422 pack .ctop.top.bar -side bottom -fill x
423 423 set cscroll .ctop.top.csb
424 424 scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
425 425 pack $cscroll -side right -fill y
426 426 panedwindow .ctop.top.clist -orient horizontal -sashpad 0 -handlesize 4
427 427 pack .ctop.top.clist -side top -fill both -expand 1
428 428 .ctop add .ctop.top
429 429 set canv .ctop.top.clist.canv
430 430 canvas $canv -height $geometry(canvh) -width $geometry(canv1) \
431 431 -bg white -bd 0 \
432 432 -yscrollincr $linespc -yscrollcommand "$cscroll set" -selectbackground grey
433 433 .ctop.top.clist add $canv
434 434 set canv2 .ctop.top.clist.canv2
435 435 canvas $canv2 -height $geometry(canvh) -width $geometry(canv2) \
436 436 -bg white -bd 0 -yscrollincr $linespc -selectbackground grey
437 437 .ctop.top.clist add $canv2
438 438 set canv3 .ctop.top.clist.canv3
439 439 canvas $canv3 -height $geometry(canvh) -width $geometry(canv3) \
440 440 -bg white -bd 0 -yscrollincr $linespc -selectbackground grey
441 441 .ctop.top.clist add $canv3
442 442 bind .ctop.top.clist <Configure> {resizeclistpanes %W %w}
443 443
444 444 set sha1entry .ctop.top.bar.sha1
445 445 set entries $sha1entry
446 446 set sha1but .ctop.top.bar.sha1label
447 447 button $sha1but -text "SHA1 ID: " -state disabled -relief flat \
448 448 -command gotocommit -width 8
449 449 $sha1but conf -disabledforeground [$sha1but cget -foreground]
450 450 pack .ctop.top.bar.sha1label -side left
451 451 entry $sha1entry -width 40 -font $textfont -textvariable sha1string
452 452 trace add variable sha1string write sha1change
453 453 pack $sha1entry -side left -pady 2
454 454
455 455 image create bitmap bm-left -data {
456 456 #define left_width 16
457 457 #define left_height 16
458 458 static unsigned char left_bits[] = {
459 459 0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
460 460 0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
461 461 0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
462 462 }
463 463 image create bitmap bm-right -data {
464 464 #define right_width 16
465 465 #define right_height 16
466 466 static unsigned char right_bits[] = {
467 467 0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
468 468 0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
469 469 0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
470 470 }
471 471 button .ctop.top.bar.leftbut -image bm-left -command goback \
472 472 -state disabled -width 26
473 473 pack .ctop.top.bar.leftbut -side left -fill y
474 474 button .ctop.top.bar.rightbut -image bm-right -command goforw \
475 475 -state disabled -width 26
476 476 pack .ctop.top.bar.rightbut -side left -fill y
477 477
478 478 button .ctop.top.bar.findbut -text "Find" -command dofind
479 479 pack .ctop.top.bar.findbut -side left
480 480 set findstring {}
481 481 set fstring .ctop.top.bar.findstring
482 482 lappend entries $fstring
483 483 entry $fstring -width 30 -font $textfont -textvariable findstring
484 484 pack $fstring -side left -expand 1 -fill x
485 485 set findtype Exact
486 486 set findtypemenu [tk_optionMenu .ctop.top.bar.findtype \
487 487 findtype Exact IgnCase Regexp]
488 488 set findloc "All fields"
489 489 tk_optionMenu .ctop.top.bar.findloc findloc "All fields" Headline \
490 490 Comments Author Committer Files Pickaxe
491 491 pack .ctop.top.bar.findloc -side right
492 492 pack .ctop.top.bar.findtype -side right
493 493 # for making sure type==Exact whenever loc==Pickaxe
494 494 trace add variable findloc write findlocchange
495 495
496 496 panedwindow .ctop.cdet -orient horizontal
497 497 .ctop add .ctop.cdet
498 498 frame .ctop.cdet.left
499 499 set ctext .ctop.cdet.left.ctext
500 500 text $ctext -bg white -state disabled -font $textfont \
501 501 -width $geometry(ctextw) -height $geometry(ctexth) \
502 502 -yscrollcommand ".ctop.cdet.left.sb set" \
503 503 -xscrollcommand ".ctop.cdet.left.hb set" -wrap none
504 504 scrollbar .ctop.cdet.left.sb -command "$ctext yview"
505 505 scrollbar .ctop.cdet.left.hb -orient horizontal -command "$ctext xview"
506 506 pack .ctop.cdet.left.sb -side right -fill y
507 507 pack .ctop.cdet.left.hb -side bottom -fill x
508 508 pack $ctext -side left -fill both -expand 1
509 509 .ctop.cdet add .ctop.cdet.left
510 510
511 511 $ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa"
512 512 if {$gaudydiff} {
513 513 $ctext tag conf hunksep -back blue -fore white
514 514 $ctext tag conf d0 -back "#ff8080"
515 515 $ctext tag conf d1 -back green
516 516 } else {
517 517 $ctext tag conf hunksep -fore blue
518 518 $ctext tag conf d0 -fore red
519 519 $ctext tag conf d1 -fore "#00a000"
520 520 $ctext tag conf m0 -fore red
521 521 $ctext tag conf m1 -fore blue
522 522 $ctext tag conf m2 -fore green
523 523 $ctext tag conf m3 -fore purple
524 524 $ctext tag conf m4 -fore brown
525 525 $ctext tag conf mmax -fore darkgrey
526 526 set mergemax 5
527 527 $ctext tag conf mresult -font [concat $textfont bold]
528 528 $ctext tag conf msep -font [concat $textfont bold]
529 529 $ctext tag conf found -back yellow
530 530 }
531 531
532 532 frame .ctop.cdet.right
533 533 set cflist .ctop.cdet.right.cfiles
534 534 listbox $cflist -bg white -selectmode extended -width $geometry(cflistw) \
535 535 -yscrollcommand ".ctop.cdet.right.sb set"
536 536 scrollbar .ctop.cdet.right.sb -command "$cflist yview"
537 537 pack .ctop.cdet.right.sb -side right -fill y
538 538 pack $cflist -side left -fill both -expand 1
539 539 .ctop.cdet add .ctop.cdet.right
540 540 bind .ctop.cdet <Configure> {resizecdetpanes %W %w}
541 541
542 542 pack .ctop -side top -fill both -expand 1
543 543
544 544 bindall <1> {selcanvline %W %x %y}
545 545 #bindall <B1-Motion> {selcanvline %W %x %y}
546 546 bindall <MouseWheel> "allcansmousewheel %D"
547 547 bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
548 548 bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
549 549 bindall <2> "allcanvs scan mark 0 %y"
550 550 bindall <B2-Motion> "allcanvs scan dragto 0 %y"
551 551 bind . <Key-Up> "selnextline -1"
552 552 bind . <Key-Down> "selnextline 1"
553 553 bind . <Key-Prior> "allcanvs yview scroll -1 pages"
554 554 bind . <Key-Next> "allcanvs yview scroll 1 pages"
555 555 bindkey <Key-Delete> "$ctext yview scroll -1 pages"
556 556 bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
557 557 bindkey <Key-space> "$ctext yview scroll 1 pages"
558 558 bindkey p "selnextline -1"
559 559 bindkey n "selnextline 1"
560 560 bindkey b "$ctext yview scroll -1 pages"
561 561 bindkey d "$ctext yview scroll 18 units"
562 562 bindkey u "$ctext yview scroll -18 units"
563 563 bindkey / {findnext 1}
564 564 bindkey <Key-Return> {findnext 0}
565 565 bindkey ? findprev
566 566 bindkey f nextfile
567 567 bind . <Control-q> doquit
568 568 bind . <Control-w> doquit
569 569 bind . <Control-f> dofind
570 570 bind . <Control-g> {findnext 0}
571 571 bind . <Control-r> findprev
572 572 bind . <Control-equal> {incrfont 1}
573 573 bind . <Control-KP_Add> {incrfont 1}
574 574 bind . <Control-minus> {incrfont -1}
575 575 bind . <Control-KP_Subtract> {incrfont -1}
576 576 bind $cflist <<ListboxSelect>> listboxsel
577 577 bind . <Destroy> {savestuff %W}
578 578 bind . <Button-1> "click %W"
579 579 bind $fstring <Key-Return> dofind
580 580 bind $sha1entry <Key-Return> gotocommit
581 581 bind $sha1entry <<PasteSelection>> clearsha1
582 582
583 583 set maincursor [. cget -cursor]
584 584 set textcursor [$ctext cget -cursor]
585 585 set curtextcursor $textcursor
586 586
587 587 set rowctxmenu .rowctxmenu
588 588 menu $rowctxmenu -tearoff 0
589 589 $rowctxmenu add command -label "Diff this -> selected" \
590 590 -command {diffvssel 0}
591 591 $rowctxmenu add command -label "Diff selected -> this" \
592 592 -command {diffvssel 1}
593 593 $rowctxmenu add command -label "Make patch" -command mkpatch
594 594 $rowctxmenu add command -label "Create tag" -command mktag
595 595 $rowctxmenu add command -label "Write commit to file" -command writecommit
596 596 }
597 597
598 598 # when we make a key binding for the toplevel, make sure
599 599 # it doesn't get triggered when that key is pressed in the
600 600 # find string entry widget.
601 601 proc bindkey {ev script} {
602 602 global entries
603 603 bind . $ev $script
604 604 set escript [bind Entry $ev]
605 605 if {$escript == {}} {
606 606 set escript [bind Entry <Key>]
607 607 }
608 608 foreach e $entries {
609 609 bind $e $ev "$escript; break"
610 610 }
611 611 }
612 612
613 613 # set the focus back to the toplevel for any click outside
614 614 # the entry widgets
615 615 proc click {w} {
616 616 global entries
617 617 foreach e $entries {
618 618 if {$w == $e} return
619 619 }
620 620 focus .
621 621 }
622 622
623 623 proc savestuff {w} {
624 624 global canv canv2 canv3 ctext cflist mainfont textfont
625 625 global stuffsaved findmergefiles gaudydiff maxgraphpct
626 626 global maxwidth
627 627
628 628 if {$stuffsaved} return
629 629 if {![winfo viewable .]} return
630 630 catch {
631 631 set f [open "~/.gitk-new" w]
632 632 puts $f [list set mainfont $mainfont]
633 633 puts $f [list set textfont $textfont]
634 634 puts $f [list set findmergefiles $findmergefiles]
635 635 puts $f [list set gaudydiff $gaudydiff]
636 636 puts $f [list set maxgraphpct $maxgraphpct]
637 637 puts $f [list set maxwidth $maxwidth]
638 638 puts $f "set geometry(width) [winfo width .ctop]"
639 639 puts $f "set geometry(height) [winfo height .ctop]"
640 640 puts $f "set geometry(canv1) [expr [winfo width $canv]-2]"
641 641 puts $f "set geometry(canv2) [expr [winfo width $canv2]-2]"
642 642 puts $f "set geometry(canv3) [expr [winfo width $canv3]-2]"
643 643 puts $f "set geometry(canvh) [expr [winfo height $canv]-2]"
644 644 set wid [expr {([winfo width $ctext] - 8) \
645 645 / [font measure $textfont "0"]}]
646 646 puts $f "set geometry(ctextw) $wid"
647 647 set wid [expr {([winfo width $cflist] - 11) \
648 648 / [font measure [$cflist cget -font] "0"]}]
649 649 puts $f "set geometry(cflistw) $wid"
650 650 close $f
651 651 file rename -force "~/.gitk-new" "~/.gitk"
652 652 }
653 653 set stuffsaved 1
654 654 }
655 655
656 656 proc resizeclistpanes {win w} {
657 657 global oldwidth
658 658 if [info exists oldwidth($win)] {
659 659 set s0 [$win sash coord 0]
660 660 set s1 [$win sash coord 1]
661 661 if {$w < 60} {
662 662 set sash0 [expr {int($w/2 - 2)}]
663 663 set sash1 [expr {int($w*5/6 - 2)}]
664 664 } else {
665 665 set factor [expr {1.0 * $w / $oldwidth($win)}]
666 666 set sash0 [expr {int($factor * [lindex $s0 0])}]
667 667 set sash1 [expr {int($factor * [lindex $s1 0])}]
668 668 if {$sash0 < 30} {
669 669 set sash0 30
670 670 }
671 671 if {$sash1 < $sash0 + 20} {
672 672 set sash1 [expr $sash0 + 20]
673 673 }
674 674 if {$sash1 > $w - 10} {
675 675 set sash1 [expr $w - 10]
676 676 if {$sash0 > $sash1 - 20} {
677 677 set sash0 [expr $sash1 - 20]
678 678 }
679 679 }
680 680 }
681 681 $win sash place 0 $sash0 [lindex $s0 1]
682 682 $win sash place 1 $sash1 [lindex $s1 1]
683 683 }
684 684 set oldwidth($win) $w
685 685 }
686 686
687 687 proc resizecdetpanes {win w} {
688 688 global oldwidth
689 689 if [info exists oldwidth($win)] {
690 690 set s0 [$win sash coord 0]
691 691 if {$w < 60} {
692 692 set sash0 [expr {int($w*3/4 - 2)}]
693 693 } else {
694 694 set factor [expr {1.0 * $w / $oldwidth($win)}]
695 695 set sash0 [expr {int($factor * [lindex $s0 0])}]
696 696 if {$sash0 < 45} {
697 697 set sash0 45
698 698 }
699 699 if {$sash0 > $w - 15} {
700 700 set sash0 [expr $w - 15]
701 701 }
702 702 }
703 703 $win sash place 0 $sash0 [lindex $s0 1]
704 704 }
705 705 set oldwidth($win) $w
706 706 }
707 707
708 708 proc allcanvs args {
709 709 global canv canv2 canv3
710 710 eval $canv $args
711 711 eval $canv2 $args
712 712 eval $canv3 $args
713 713 }
714 714
715 715 proc bindall {event action} {
716 716 global canv canv2 canv3
717 717 bind $canv $event $action
718 718 bind $canv2 $event $action
719 719 bind $canv3 $event $action
720 720 }
721 721
722 722 proc about {} {
723 723 set w .about
724 724 if {[winfo exists $w]} {
725 725 raise $w
726 726 return
727 727 }
728 728 toplevel $w
729 729 wm title $w "About gitk"
730 730 message $w.m -text {
731 731 Gitk version 1.2
732 732
733 733 Copyright � 2005 Paul Mackerras
734 734
735 735 Use and redistribute under the terms of the GNU General Public License} \
736 736 -justify center -aspect 400
737 737 pack $w.m -side top -fill x -padx 20 -pady 20
738 738 button $w.ok -text Close -command "destroy $w"
739 739 pack $w.ok -side bottom
740 740 }
741 741
742 742 proc assigncolor {id} {
743 743 global commitinfo colormap commcolors colors nextcolor
744 744 global parents nparents children nchildren
745 745 global cornercrossings crossings
746 746
747 747 if [info exists colormap($id)] return
748 748 set ncolors [llength $colors]
749 749 if {$nparents($id) <= 1 && $nchildren($id) == 1} {
750 750 set child [lindex $children($id) 0]
751 751 if {[info exists colormap($child)]
752 752 && $nparents($child) == 1} {
753 753 set colormap($id) $colormap($child)
754 754 return
755 755 }
756 756 }
757 757 set badcolors {}
758 758 if {[info exists cornercrossings($id)]} {
759 759 foreach x $cornercrossings($id) {
760 760 if {[info exists colormap($x)]
761 761 && [lsearch -exact $badcolors $colormap($x)] < 0} {
762 762 lappend badcolors $colormap($x)
763 763 }
764 764 }
765 765 if {[llength $badcolors] >= $ncolors} {
766 766 set badcolors {}
767 767 }
768 768 }
769 769 set origbad $badcolors
770 770 if {[llength $badcolors] < $ncolors - 1} {
771 771 if {[info exists crossings($id)]} {
772 772 foreach x $crossings($id) {
773 773 if {[info exists colormap($x)]
774 774 && [lsearch -exact $badcolors $colormap($x)] < 0} {
775 775 lappend badcolors $colormap($x)
776 776 }
777 777 }
778 778 if {[llength $badcolors] >= $ncolors} {
779 779 set badcolors $origbad
780 780 }
781 781 }
782 782 set origbad $badcolors
783 783 }
784 784 if {[llength $badcolors] < $ncolors - 1} {
785 785 foreach child $children($id) {
786 786 if {[info exists colormap($child)]
787 787 && [lsearch -exact $badcolors $colormap($child)] < 0} {
788 788 lappend badcolors $colormap($child)
789 789 }
790 790 if {[info exists parents($child)]} {
791 791 foreach p $parents($child) {
792 792 if {[info exists colormap($p)]
793 793 && [lsearch -exact $badcolors $colormap($p)] < 0} {
794 794 lappend badcolors $colormap($p)
795 795 }
796 796 }
797 797 }
798 798 }
799 799 if {[llength $badcolors] >= $ncolors} {
800 800 set badcolors $origbad
801 801 }
802 802 }
803 803 for {set i 0} {$i <= $ncolors} {incr i} {
804 804 set c [lindex $colors $nextcolor]
805 805 if {[incr nextcolor] >= $ncolors} {
806 806 set nextcolor 0
807 807 }
808 808 if {[lsearch -exact $badcolors $c]} break
809 809 }
810 810 set colormap($id) $c
811 811 }
812 812
813 813 proc initgraph {} {
814 814 global canvy canvy0 lineno numcommits nextcolor linespc
815 815 global mainline mainlinearrow sidelines
816 816 global nchildren ncleft
817 817 global displist nhyperspace
818 818
819 819 allcanvs delete all
820 820 set nextcolor 0
821 821 set canvy $canvy0
822 822 set lineno -1
823 823 set numcommits 0
824 824 catch {unset mainline}
825 825 catch {unset mainlinearrow}
826 826 catch {unset sidelines}
827 827 foreach id [array names nchildren] {
828 828 set ncleft($id) $nchildren($id)
829 829 }
830 830 set displist {}
831 831 set nhyperspace 0
832 832 }
833 833
834 834 proc bindline {t id} {
835 835 global canv
836 836
837 837 $canv bind $t <Enter> "lineenter %x %y $id"
838 838 $canv bind $t <Motion> "linemotion %x %y $id"
839 839 $canv bind $t <Leave> "lineleave $id"
840 840 $canv bind $t <Button-1> "lineclick %x %y $id 1"
841 841 }
842 842
843 843 proc drawlines {id xtra} {
844 844 global mainline mainlinearrow sidelines lthickness colormap canv
845 845
846 846 $canv delete lines.$id
847 847 if {[info exists mainline($id)]} {
848 848 set t [$canv create line $mainline($id) \
849 849 -width [expr {($xtra + 1) * $lthickness}] \
850 850 -fill $colormap($id) -tags lines.$id \
851 851 -arrow $mainlinearrow($id)]
852 852 $canv lower $t
853 853 bindline $t $id
854 854 }
855 855 if {[info exists sidelines($id)]} {
856 856 foreach ls $sidelines($id) {
857 857 set coords [lindex $ls 0]
858 858 set thick [lindex $ls 1]
859 859 set arrow [lindex $ls 2]
860 860 set t [$canv create line $coords -fill $colormap($id) \
861 861 -width [expr {($thick + $xtra) * $lthickness}] \
862 862 -arrow $arrow -tags lines.$id]
863 863 $canv lower $t
864 864 bindline $t $id
865 865 }
866 866 }
867 867 }
868 868
869 869 # level here is an index in displist
870 870 proc drawcommitline {level} {
871 871 global parents children nparents displist
872 872 global canv canv2 canv3 mainfont namefont canvy linespc
873 873 global lineid linehtag linentag linedtag commitinfo
874 874 global colormap numcommits currentparents dupparents
875 875 global idtags idline idheads idotherrefs
876 876 global lineno lthickness mainline mainlinearrow sidelines
877 877 global commitlisted rowtextx idpos lastuse displist
878 878 global oldnlines olddlevel olddisplist
879 879
880 880 incr numcommits
881 881 incr lineno
882 882 set id [lindex $displist $level]
883 883 set lastuse($id) $lineno
884 884 set lineid($lineno) $id
885 885 set idline($id) $lineno
886 886 set ofill [expr {[info exists commitlisted($id)]? "blue": "white"}]
887 887 if {![info exists commitinfo($id)]} {
888 888 readcommit $id
889 889 if {![info exists commitinfo($id)]} {
890 890 set commitinfo($id) {"No commit information available"}
891 891 set nparents($id) 0
892 892 }
893 893 }
894 894 assigncolor $id
895 895 set currentparents {}
896 896 set dupparents {}
897 897 if {[info exists commitlisted($id)] && [info exists parents($id)]} {
898 898 foreach p $parents($id) {
899 899 if {[lsearch -exact $currentparents $p] < 0} {
900 900 lappend currentparents $p
901 901 } else {
902 902 # remember that this parent was listed twice
903 903 lappend dupparents $p
904 904 }
905 905 }
906 906 }
907 907 set x [xcoord $level $level $lineno]
908 908 set y1 $canvy
909 909 set canvy [expr $canvy + $linespc]
910 910 allcanvs conf -scrollregion \
911 911 [list 0 0 0 [expr $y1 + 0.5 * $linespc + 2]]
912 912 if {[info exists mainline($id)]} {
913 913 lappend mainline($id) $x $y1
914 914 if {$mainlinearrow($id) ne "none"} {
915 915 set mainline($id) [trimdiagstart $mainline($id)]
916 916 }
917 917 }
918 918 drawlines $id 0
919 919 set orad [expr {$linespc / 3}]
920 920 set t [$canv create oval [expr $x - $orad] [expr $y1 - $orad] \
921 921 [expr $x + $orad - 1] [expr $y1 + $orad - 1] \
922 922 -fill $ofill -outline black -width 1]
923 923 $canv raise $t
924 924 $canv bind $t <1> {selcanvline {} %x %y}
925 925 set xt [xcoord [llength $displist] $level $lineno]
926 926 if {[llength $currentparents] > 2} {
927 927 set xt [expr {$xt + ([llength $currentparents] - 2) * $linespc}]
928 928 }
929 929 set rowtextx($lineno) $xt
930 930 set idpos($id) [list $x $xt $y1]
931 931 if {[info exists idtags($id)] || [info exists idheads($id)]
932 932 || [info exists idotherrefs($id)]} {
933 933 set xt [drawtags $id $x $xt $y1]
934 934 }
935 935 set headline [lindex $commitinfo($id) 0]
936 936 set name [lindex $commitinfo($id) 1]
937 937 set date [lindex $commitinfo($id) 2]
938 938 set linehtag($lineno) [$canv create text $xt $y1 -anchor w \
939 939 -text $headline -font $mainfont ]
940 940 $canv bind $linehtag($lineno) <Button-3> "rowmenu %X %Y $id"
941 941 set linentag($lineno) [$canv2 create text 3 $y1 -anchor w \
942 942 -text $name -font $namefont]
943 943 set linedtag($lineno) [$canv3 create text 3 $y1 -anchor w \
944 944 -text $date -font $mainfont]
945 945
946 946 set olddlevel $level
947 947 set olddisplist $displist
948 948 set oldnlines [llength $displist]
949 949 }
950 950
951 951 proc drawtags {id x xt y1} {
952 952 global idtags idheads idotherrefs
953 953 global linespc lthickness
954 954 global canv mainfont idline rowtextx
955 955
956 956 set marks {}
957 957 set ntags 0
958 958 set nheads 0
959 959 if {[info exists idtags($id)]} {
960 960 set marks $idtags($id)
961 961 set ntags [llength $marks]
962 962 }
963 963 if {[info exists idheads($id)]} {
964 964 set marks [concat $marks $idheads($id)]
965 965 set nheads [llength $idheads($id)]
966 966 }
967 967 if {[info exists idotherrefs($id)]} {
968 968 set marks [concat $marks $idotherrefs($id)]
969 969 }
970 970 if {$marks eq {}} {
971 971 return $xt
972 972 }
973 973
974 974 set delta [expr {int(0.5 * ($linespc - $lthickness))}]
975 975 set yt [expr $y1 - 0.5 * $linespc]
976 976 set yb [expr $yt + $linespc - 1]
977 977 set xvals {}
978 978 set wvals {}
979 979 foreach tag $marks {
980 980 set wid [font measure $mainfont $tag]
981 981 lappend xvals $xt
982 982 lappend wvals $wid
983 983 set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
984 984 }
985 985 set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
986 986 -width $lthickness -fill black -tags tag.$id]
987 987 $canv lower $t
988 988 foreach tag $marks x $xvals wid $wvals {
989 989 set xl [expr $x + $delta]
990 990 set xr [expr $x + $delta + $wid + $lthickness]
991 991 if {[incr ntags -1] >= 0} {
992 992 # draw a tag
993 993 set t [$canv create polygon $x [expr $yt + $delta] $xl $yt \
994 994 $xr $yt $xr $yb $xl $yb $x [expr $yb - $delta] \
995 995 -width 1 -outline black -fill yellow -tags tag.$id]
996 996 $canv bind $t <1> [list showtag $tag 1]
997 997 set rowtextx($idline($id)) [expr {$xr + $linespc}]
998 998 } else {
999 999 # draw a head or other ref
1000 1000 if {[incr nheads -1] >= 0} {
1001 1001 set col green
1002 1002 } else {
1003 1003 set col "#ddddff"
1004 1004 }
1005 1005 set xl [expr $xl - $delta/2]
1006 1006 $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
1007 1007 -width 1 -outline black -fill $col -tags tag.$id
1008 1008 }
1009 1009 set t [$canv create text $xl $y1 -anchor w -text $tag \
1010 1010 -font $mainfont -tags tag.$id]
1011 1011 if {$ntags >= 0} {
1012 1012 $canv bind $t <1> [list showtag $tag 1]
1013 1013 }
1014 1014 }
1015 1015 return $xt
1016 1016 }
1017 1017
1018 1018 proc notecrossings {id lo hi corner} {
1019 1019 global olddisplist crossings cornercrossings
1020 1020
1021 1021 for {set i $lo} {[incr i] < $hi} {} {
1022 1022 set p [lindex $olddisplist $i]
1023 1023 if {$p == {}} continue
1024 1024 if {$i == $corner} {
1025 1025 if {![info exists cornercrossings($id)]
1026 1026 || [lsearch -exact $cornercrossings($id) $p] < 0} {
1027 1027 lappend cornercrossings($id) $p
1028 1028 }
1029 1029 if {![info exists cornercrossings($p)]
1030 1030 || [lsearch -exact $cornercrossings($p) $id] < 0} {
1031 1031 lappend cornercrossings($p) $id
1032 1032 }
1033 1033 } else {
1034 1034 if {![info exists crossings($id)]
1035 1035 || [lsearch -exact $crossings($id) $p] < 0} {
1036 1036 lappend crossings($id) $p
1037 1037 }
1038 1038 if {![info exists crossings($p)]
1039 1039 || [lsearch -exact $crossings($p) $id] < 0} {
1040 1040 lappend crossings($p) $id
1041 1041 }
1042 1042 }
1043 1043 }
1044 1044 }
1045 1045
1046 1046 proc xcoord {i level ln} {
1047 1047 global canvx0 xspc1 xspc2
1048 1048
1049 1049 set x [expr {$canvx0 + $i * $xspc1($ln)}]
1050 1050 if {$i > 0 && $i == $level} {
1051 1051 set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
1052 1052 } elseif {$i > $level} {
1053 1053 set x [expr {$x + $xspc2 - $xspc1($ln)}]
1054 1054 }
1055 1055 return $x
1056 1056 }
1057 1057
1058 1058 # it seems Tk can't draw arrows on the end of diagonal line segments...
1059 1059 proc trimdiagend {line} {
1060 1060 while {[llength $line] > 4} {
1061 1061 set x1 [lindex $line end-3]
1062 1062 set y1 [lindex $line end-2]
1063 1063 set x2 [lindex $line end-1]
1064 1064 set y2 [lindex $line end]
1065 1065 if {($x1 == $x2) != ($y1 == $y2)} break
1066 1066 set line [lreplace $line end-1 end]
1067 1067 }
1068 1068 return $line
1069 1069 }
1070 1070
1071 1071 proc trimdiagstart {line} {
1072 1072 while {[llength $line] > 4} {
1073 1073 set x1 [lindex $line 0]
1074 1074 set y1 [lindex $line 1]
1075 1075 set x2 [lindex $line 2]
1076 1076 set y2 [lindex $line 3]
1077 1077 if {($x1 == $x2) != ($y1 == $y2)} break
1078 1078 set line [lreplace $line 0 1]
1079 1079 }
1080 1080 return $line
1081 1081 }
1082 1082
1083 1083 proc drawslants {id needonscreen nohs} {
1084 1084 global canv mainline mainlinearrow sidelines
1085 1085 global canvx0 canvy xspc1 xspc2 lthickness
1086 1086 global currentparents dupparents
1087 1087 global lthickness linespc canvy colormap lineno geometry
1088 1088 global maxgraphpct maxwidth
1089 1089 global displist onscreen lastuse
1090 1090 global parents commitlisted
1091 1091 global oldnlines olddlevel olddisplist
1092 1092 global nhyperspace numcommits nnewparents
1093 1093
1094 1094 if {$lineno < 0} {
1095 1095 lappend displist $id
1096 1096 set onscreen($id) 1
1097 1097 return 0
1098 1098 }
1099 1099
1100 1100 set y1 [expr {$canvy - $linespc}]
1101 1101 set y2 $canvy
1102 1102
1103 1103 # work out what we need to get back on screen
1104 1104 set reins {}
1105 1105 if {$onscreen($id) < 0} {
1106 1106 # next to do isn't displayed, better get it on screen...
1107 1107 lappend reins [list $id 0]
1108 1108 }
1109 1109 # make sure all the previous commits's parents are on the screen
1110 1110 foreach p $currentparents {
1111 1111 if {$onscreen($p) < 0} {
1112 1112 lappend reins [list $p 0]
1113 1113 }
1114 1114 }
1115 1115 # bring back anything requested by caller
1116 1116 if {$needonscreen ne {}} {
1117 1117 lappend reins $needonscreen
1118 1118 }
1119 1119
1120 1120 # try the shortcut
1121 1121 if {$currentparents == $id && $onscreen($id) == 0 && $reins eq {}} {
1122 1122 set dlevel $olddlevel
1123 1123 set x [xcoord $dlevel $dlevel $lineno]
1124 1124 set mainline($id) [list $x $y1]
1125 1125 set mainlinearrow($id) none
1126 1126 set lastuse($id) $lineno
1127 1127 set displist [lreplace $displist $dlevel $dlevel $id]
1128 1128 set onscreen($id) 1
1129 1129 set xspc1([expr {$lineno + 1}]) $xspc1($lineno)
1130 1130 return $dlevel
1131 1131 }
1132 1132
1133 1133 # update displist
1134 1134 set displist [lreplace $displist $olddlevel $olddlevel]
1135 1135 set j $olddlevel
1136 1136 foreach p $currentparents {
1137 1137 set lastuse($p) $lineno
1138 1138 if {$onscreen($p) == 0} {
1139 1139 set displist [linsert $displist $j $p]
1140 1140 set onscreen($p) 1
1141 1141 incr j
1142 1142 }
1143 1143 }
1144 1144 if {$onscreen($id) == 0} {
1145 1145 lappend displist $id
1146 1146 set onscreen($id) 1
1147 1147 }
1148 1148
1149 1149 # remove the null entry if present
1150 1150 set nullentry [lsearch -exact $displist {}]
1151 1151 if {$nullentry >= 0} {
1152 1152 set displist [lreplace $displist $nullentry $nullentry]
1153 1153 }
1154 1154
1155 1155 # bring back the ones we need now (if we did it earlier
1156 1156 # it would change displist and invalidate olddlevel)
1157 1157 foreach pi $reins {
1158 1158 # test again in case of duplicates in reins
1159 1159 set p [lindex $pi 0]
1160 1160 if {$onscreen($p) < 0} {
1161 1161 set onscreen($p) 1
1162 1162 set lastuse($p) $lineno
1163 1163 set displist [linsert $displist [lindex $pi 1] $p]
1164 1164 incr nhyperspace -1
1165 1165 }
1166 1166 }
1167 1167
1168 1168 set lastuse($id) $lineno
1169 1169
1170 1170 # see if we need to make any lines jump off into hyperspace
1171 1171 set displ [llength $displist]
1172 1172 if {$displ > $maxwidth} {
1173 1173 set ages {}
1174 1174 foreach x $displist {
1175 1175 lappend ages [list $lastuse($x) $x]
1176 1176 }
1177 1177 set ages [lsort -integer -index 0 $ages]
1178 1178 set k 0
1179 1179 while {$displ > $maxwidth} {
1180 1180 set use [lindex $ages $k 0]
1181 1181 set victim [lindex $ages $k 1]
1182 1182 if {$use >= $lineno - 5} break
1183 1183 incr k
1184 1184 if {[lsearch -exact $nohs $victim] >= 0} continue
1185 1185 set i [lsearch -exact $displist $victim]
1186 1186 set displist [lreplace $displist $i $i]
1187 1187 set onscreen($victim) -1
1188 1188 incr nhyperspace
1189 1189 incr displ -1
1190 1190 if {$i < $nullentry} {
1191 1191 incr nullentry -1
1192 1192 }
1193 1193 set x [lindex $mainline($victim) end-1]
1194 1194 lappend mainline($victim) $x $y1
1195 1195 set line [trimdiagend $mainline($victim)]
1196 1196 set arrow "last"
1197 1197 if {$mainlinearrow($victim) ne "none"} {
1198 1198 set line [trimdiagstart $line]
1199 1199 set arrow "both"
1200 1200 }
1201 1201 lappend sidelines($victim) [list $line 1 $arrow]
1202 1202 unset mainline($victim)
1203 1203 }
1204 1204 }
1205 1205
1206 1206 set dlevel [lsearch -exact $displist $id]
1207 1207
1208 1208 # If we are reducing, put in a null entry
1209 1209 if {$displ < $oldnlines} {
1210 1210 # does the next line look like a merge?
1211 1211 # i.e. does it have > 1 new parent?
1212 1212 if {$nnewparents($id) > 1} {
1213 1213 set i [expr {$dlevel + 1}]
1214 1214 } elseif {$nnewparents([lindex $olddisplist $olddlevel]) == 0} {
1215 1215 set i $olddlevel
1216 1216 if {$nullentry >= 0 && $nullentry < $i} {
1217 1217 incr i -1
1218 1218 }
1219 1219 } elseif {$nullentry >= 0} {
1220 1220 set i $nullentry
1221 1221 while {$i < $displ
1222 1222 && [lindex $olddisplist $i] == [lindex $displist $i]} {
1223 1223 incr i
1224 1224 }
1225 1225 } else {
1226 1226 set i $olddlevel
1227 1227 if {$dlevel >= $i} {
1228 1228 incr i
1229 1229 }
1230 1230 }
1231 1231 if {$i < $displ} {
1232 1232 set displist [linsert $displist $i {}]
1233 1233 incr displ
1234 1234 if {$dlevel >= $i} {
1235 1235 incr dlevel
1236 1236 }
1237 1237 }
1238 1238 }
1239 1239
1240 1240 # decide on the line spacing for the next line
1241 1241 set lj [expr {$lineno + 1}]
1242 1242 set maxw [expr {$maxgraphpct * $geometry(canv1) / 100}]
1243 1243 if {$displ <= 1 || $canvx0 + $displ * $xspc2 <= $maxw} {
1244 1244 set xspc1($lj) $xspc2
1245 1245 } else {
1246 1246 set xspc1($lj) [expr {($maxw - $canvx0 - $xspc2) / ($displ - 1)}]
1247 1247 if {$xspc1($lj) < $lthickness} {
1248 1248 set xspc1($lj) $lthickness
1249 1249 }
1250 1250 }
1251 1251
1252 1252 foreach idi $reins {
1253 1253 set id [lindex $idi 0]
1254 1254 set j [lsearch -exact $displist $id]
1255 1255 set xj [xcoord $j $dlevel $lj]
1256 1256 set mainline($id) [list $xj $y2]
1257 1257 set mainlinearrow($id) first
1258 1258 }
1259 1259
1260 1260 set i -1
1261 1261 foreach id $olddisplist {
1262 1262 incr i
1263 1263 if {$id == {}} continue
1264 1264 if {$onscreen($id) <= 0} continue
1265 1265 set xi [xcoord $i $olddlevel $lineno]
1266 1266 if {$i == $olddlevel} {
1267 1267 foreach p $currentparents {
1268 1268 set j [lsearch -exact $displist $p]
1269 1269 set coords [list $xi $y1]
1270 1270 set xj [xcoord $j $dlevel $lj]
1271 1271 if {$xj < $xi - $linespc} {
1272 1272 lappend coords [expr {$xj + $linespc}] $y1
1273 1273 notecrossings $p $j $i [expr {$j + 1}]
1274 1274 } elseif {$xj > $xi + $linespc} {
1275 1275 lappend coords [expr {$xj - $linespc}] $y1
1276 1276 notecrossings $p $i $j [expr {$j - 1}]
1277 1277 }
1278 1278 if {[lsearch -exact $dupparents $p] >= 0} {
1279 1279 # draw a double-width line to indicate the doubled parent
1280 1280 lappend coords $xj $y2
1281 1281 lappend sidelines($p) [list $coords 2 none]
1282 1282 if {![info exists mainline($p)]} {
1283 1283 set mainline($p) [list $xj $y2]
1284 1284 set mainlinearrow($p) none
1285 1285 }
1286 1286 } else {
1287 1287 # normal case, no parent duplicated
1288 1288 set yb $y2
1289 1289 set dx [expr {abs($xi - $xj)}]
1290 1290 if {0 && $dx < $linespc} {
1291 1291 set yb [expr {$y1 + $dx}]
1292 1292 }
1293 1293 if {![info exists mainline($p)]} {
1294 1294 if {$xi != $xj} {
1295 1295 lappend coords $xj $yb
1296 1296 }
1297 1297 set mainline($p) $coords
1298 1298 set mainlinearrow($p) none
1299 1299 } else {
1300 1300 lappend coords $xj $yb
1301 1301 if {$yb < $y2} {
1302 1302 lappend coords $xj $y2
1303 1303 }
1304 1304 lappend sidelines($p) [list $coords 1 none]
1305 1305 }
1306 1306 }
1307 1307 }
1308 1308 } else {
1309 1309 set j $i
1310 1310 if {[lindex $displist $i] != $id} {
1311 1311 set j [lsearch -exact $displist $id]
1312 1312 }
1313 1313 if {$j != $i || $xspc1($lineno) != $xspc1($lj)
1314 1314 || ($olddlevel < $i && $i < $dlevel)
1315 1315 || ($dlevel < $i && $i < $olddlevel)} {
1316 1316 set xj [xcoord $j $dlevel $lj]
1317 1317 lappend mainline($id) $xi $y1 $xj $y2
1318 1318 }
1319 1319 }
1320 1320 }
1321 1321 return $dlevel
1322 1322 }
1323 1323
1324 1324 # search for x in a list of lists
1325 1325 proc llsearch {llist x} {
1326 1326 set i 0
1327 1327 foreach l $llist {
1328 1328 if {$l == $x || [lsearch -exact $l $x] >= 0} {
1329 1329 return $i
1330 1330 }
1331 1331 incr i
1332 1332 }
1333 1333 return -1
1334 1334 }
1335 1335
1336 1336 proc drawmore {reading} {
1337 1337 global displayorder numcommits ncmupdate nextupdate
1338 1338 global stopped nhyperspace parents commitlisted
1339 1339 global maxwidth onscreen displist currentparents olddlevel
1340 1340
1341 1341 set n [llength $displayorder]
1342 1342 while {$numcommits < $n} {
1343 1343 set id [lindex $displayorder $numcommits]
1344 1344 set ctxend [expr {$numcommits + 10}]
1345 1345 if {!$reading && $ctxend > $n} {
1346 1346 set ctxend $n
1347 1347 }
1348 1348 set dlist {}
1349 1349 if {$numcommits > 0} {
1350 1350 set dlist [lreplace $displist $olddlevel $olddlevel]
1351 1351 set i $olddlevel
1352 1352 foreach p $currentparents {
1353 1353 if {$onscreen($p) == 0} {
1354 1354 set dlist [linsert $dlist $i $p]
1355 1355 incr i
1356 1356 }
1357 1357 }
1358 1358 }
1359 1359 set nohs {}
1360 1360 set reins {}
1361 1361 set isfat [expr {[llength $dlist] > $maxwidth}]
1362 1362 if {$nhyperspace > 0 || $isfat} {
1363 1363 if {$ctxend > $n} break
1364 1364 # work out what to bring back and
1365 1365 # what we want to don't want to send into hyperspace
1366 1366 set room 1
1367 1367 for {set k $numcommits} {$k < $ctxend} {incr k} {
1368 1368 set x [lindex $displayorder $k]
1369 1369 set i [llsearch $dlist $x]
1370 1370 if {$i < 0} {
1371 1371 set i [llength $dlist]
1372 1372 lappend dlist $x
1373 1373 }
1374 1374 if {[lsearch -exact $nohs $x] < 0} {
1375 1375 lappend nohs $x
1376 1376 }
1377 1377 if {$reins eq {} && $onscreen($x) < 0 && $room} {
1378 1378 set reins [list $x $i]
1379 1379 }
1380 1380 set newp {}
1381 1381 if {[info exists commitlisted($x)]} {
1382 1382 set right 0
1383 1383 foreach p $parents($x) {
1384 1384 if {[llsearch $dlist $p] < 0} {
1385 1385 lappend newp $p
1386 1386 if {[lsearch -exact $nohs $p] < 0} {
1387 1387 lappend nohs $p
1388 1388 }
1389 1389 if {$reins eq {} && $onscreen($p) < 0 && $room} {
1390 1390 set reins [list $p [expr {$i + $right}]]
1391 1391 }
1392 1392 }
1393 1393 set right 1
1394 1394 }
1395 1395 }
1396 1396 set l [lindex $dlist $i]
1397 1397 if {[llength $l] == 1} {
1398 1398 set l $newp
1399 1399 } else {
1400 1400 set j [lsearch -exact $l $x]
1401 1401 set l [concat [lreplace $l $j $j] $newp]
1402 1402 }
1403 1403 set dlist [lreplace $dlist $i $i $l]
1404 1404 if {$room && $isfat && [llength $newp] <= 1} {
1405 1405 set room 0
1406 1406 }
1407 1407 }
1408 1408 }
1409 1409
1410 1410 set dlevel [drawslants $id $reins $nohs]
1411 1411 drawcommitline $dlevel
1412 1412 if {[clock clicks -milliseconds] >= $nextupdate
1413 1413 && $numcommits >= $ncmupdate} {
1414 1414 doupdate $reading
1415 1415 if {$stopped} break
1416 1416 }
1417 1417 }
1418 1418 }
1419 1419
1420 1420 # level here is an index in todo
1421 1421 proc updatetodo {level noshortcut} {
1422 1422 global ncleft todo nnewparents
1423 1423 global commitlisted parents onscreen
1424 1424
1425 1425 set id [lindex $todo $level]
1426 1426 set olds {}
1427 1427 if {[info exists commitlisted($id)]} {
1428 1428 foreach p $parents($id) {
1429 1429 if {[lsearch -exact $olds $p] < 0} {
1430 1430 lappend olds $p
1431 1431 }
1432 1432 }
1433 1433 }
1434 1434 if {!$noshortcut && [llength $olds] == 1} {
1435 1435 set p [lindex $olds 0]
1436 1436 if {$ncleft($p) == 1 && [lsearch -exact $todo $p] < 0} {
1437 1437 set ncleft($p) 0
1438 1438 set todo [lreplace $todo $level $level $p]
1439 1439 set onscreen($p) 0
1440 1440 set nnewparents($id) 1
1441 1441 return 0
1442 1442 }
1443 1443 }
1444 1444
1445 1445 set todo [lreplace $todo $level $level]
1446 1446 set i $level
1447 1447 set n 0
1448 1448 foreach p $olds {
1449 1449 incr ncleft($p) -1
1450 1450 set k [lsearch -exact $todo $p]
1451 1451 if {$k < 0} {
1452 1452 set todo [linsert $todo $i $p]
1453 1453 set onscreen($p) 0
1454 1454 incr i
1455 1455 incr n
1456 1456 }
1457 1457 }
1458 1458 set nnewparents($id) $n
1459 1459
1460 1460 return 1
1461 1461 }
1462 1462
1463 1463 proc decidenext {{noread 0}} {
1464 1464 global ncleft todo
1465 1465 global datemode cdate
1466 1466 global commitinfo
1467 1467
1468 1468 # choose which one to do next time around
1469 1469 set todol [llength $todo]
1470 1470 set level -1
1471 1471 set latest {}
1472 1472 for {set k $todol} {[incr k -1] >= 0} {} {
1473 1473 set p [lindex $todo $k]
1474 1474 if {$ncleft($p) == 0} {
1475 1475 if {$datemode} {
1476 1476 if {![info exists commitinfo($p)]} {
1477 1477 if {$noread} {
1478 1478 return {}
1479 1479 }
1480 1480 readcommit $p
1481 1481 }
1482 1482 if {$latest == {} || $cdate($p) > $latest} {
1483 1483 set level $k
1484 1484 set latest $cdate($p)
1485 1485 }
1486 1486 } else {
1487 1487 set level $k
1488 1488 break
1489 1489 }
1490 1490 }
1491 1491 }
1492 1492 if {$level < 0} {
1493 1493 if {$todo != {}} {
1494 1494 puts "ERROR: none of the pending commits can be done yet:"
1495 1495 foreach p $todo {
1496 1496 puts " $p ($ncleft($p))"
1497 1497 }
1498 1498 }
1499 1499 return -1
1500 1500 }
1501 1501
1502 1502 return $level
1503 1503 }
1504 1504
1505 1505 proc drawcommit {id} {
1506 1506 global phase todo nchildren datemode nextupdate
1507 1507 global numcommits ncmupdate displayorder todo onscreen
1508 1508
1509 1509 if {$phase != "incrdraw"} {
1510 1510 set phase incrdraw
1511 1511 set displayorder {}
1512 1512 set todo {}
1513 1513 initgraph
1514 1514 }
1515 1515 if {$nchildren($id) == 0} {
1516 1516 lappend todo $id
1517 1517 set onscreen($id) 0
1518 1518 }
1519 1519 set level [decidenext 1]
1520 1520 if {$level == {} || $id != [lindex $todo $level]} {
1521 1521 return
1522 1522 }
1523 1523 while 1 {
1524 1524 lappend displayorder [lindex $todo $level]
1525 1525 if {[updatetodo $level $datemode]} {
1526 1526 set level [decidenext 1]
1527 1527 if {$level == {}} break
1528 1528 }
1529 1529 set id [lindex $todo $level]
1530 1530 if {![info exists commitlisted($id)]} {
1531 1531 break
1532 1532 }
1533 1533 }
1534 1534 drawmore 1
1535 1535 }
1536 1536
1537 1537 proc finishcommits {} {
1538 1538 global phase
1539 1539 global canv mainfont ctext maincursor textcursor
1540 1540
1541 1541 if {$phase != "incrdraw"} {
1542 1542 $canv delete all
1543 1543 $canv create text 3 3 -anchor nw -text "No commits selected" \
1544 1544 -font $mainfont -tags textitems
1545 1545 set phase {}
1546 1546 } else {
1547 1547 drawrest
1548 1548 }
1549 1549 . config -cursor $maincursor
1550 1550 settextcursor $textcursor
1551 1551 }
1552 1552
1553 1553 # Don't change the text pane cursor if it is currently the hand cursor,
1554 1554 # showing that we are over a sha1 ID link.
1555 1555 proc settextcursor {c} {
1556 1556 global ctext curtextcursor
1557 1557
1558 1558 if {[$ctext cget -cursor] == $curtextcursor} {
1559 1559 $ctext config -cursor $c
1560 1560 }
1561 1561 set curtextcursor $c
1562 1562 }
1563 1563
1564 1564 proc drawgraph {} {
1565 1565 global nextupdate startmsecs ncmupdate
1566 1566 global displayorder onscreen
1567 1567
1568 1568 if {$displayorder == {}} return
1569 1569 set startmsecs [clock clicks -milliseconds]
1570 1570 set nextupdate [expr $startmsecs + 100]
1571 1571 set ncmupdate 1
1572 1572 initgraph
1573 1573 foreach id $displayorder {
1574 1574 set onscreen($id) 0
1575 1575 }
1576 1576 drawmore 0
1577 1577 }
1578 1578
1579 1579 proc drawrest {} {
1580 1580 global phase stopped redisplaying selectedline
1581 1581 global datemode todo displayorder
1582 1582 global numcommits ncmupdate
1583 1583 global nextupdate startmsecs
1584 1584
1585 1585 set level [decidenext]
1586 1586 if {$level >= 0} {
1587 1587 set phase drawgraph
1588 1588 while 1 {
1589 1589 lappend displayorder [lindex $todo $level]
1590 1590 set hard [updatetodo $level $datemode]
1591 1591 if {$hard} {
1592 1592 set level [decidenext]
1593 1593 if {$level < 0} break
1594 1594 }
1595 1595 }
1596 1596 drawmore 0
1597 1597 }
1598 1598 set phase {}
1599 1599 set drawmsecs [expr [clock clicks -milliseconds] - $startmsecs]
1600 1600 #puts "overall $drawmsecs ms for $numcommits commits"
1601 1601 if {$redisplaying} {
1602 1602 if {$stopped == 0 && [info exists selectedline]} {
1603 1603 selectline $selectedline 0
1604 1604 }
1605 1605 if {$stopped == 1} {
1606 1606 set stopped 0
1607 1607 after idle drawgraph
1608 1608 } else {
1609 1609 set redisplaying 0
1610 1610 }
1611 1611 }
1612 1612 }
1613 1613
1614 1614 proc findmatches {f} {
1615 1615 global findtype foundstring foundstrlen
1616 1616 if {$findtype == "Regexp"} {
1617 1617 set matches [regexp -indices -all -inline $foundstring $f]
1618 1618 } else {
1619 1619 if {$findtype == "IgnCase"} {
1620 1620 set str [string tolower $f]
1621 1621 } else {
1622 1622 set str $f
1623 1623 }
1624 1624 set matches {}
1625 1625 set i 0
1626 1626 while {[set j [string first $foundstring $str $i]] >= 0} {
1627 1627 lappend matches [list $j [expr $j+$foundstrlen-1]]
1628 1628 set i [expr $j + $foundstrlen]
1629 1629 }
1630 1630 }
1631 1631 return $matches
1632 1632 }
1633 1633
1634 1634 proc dofind {} {
1635 1635 global findtype findloc findstring markedmatches commitinfo
1636 1636 global numcommits lineid linehtag linentag linedtag
1637 1637 global mainfont namefont canv canv2 canv3 selectedline
1638 1638 global matchinglines foundstring foundstrlen
1639 1639
1640 1640 stopfindproc
1641 1641 unmarkmatches
1642 1642 focus .
1643 1643 set matchinglines {}
1644 1644 if {$findloc == "Pickaxe"} {
1645 1645 findpatches
1646 1646 return
1647 1647 }
1648 1648 if {$findtype == "IgnCase"} {
1649 1649 set foundstring [string tolower $findstring]
1650 1650 } else {
1651 1651 set foundstring $findstring
1652 1652 }
1653 1653 set foundstrlen [string length $findstring]
1654 1654 if {$foundstrlen == 0} return
1655 1655 if {$findloc == "Files"} {
1656 1656 findfiles
1657 1657 return
1658 1658 }
1659 1659 if {![info exists selectedline]} {
1660 1660 set oldsel -1
1661 1661 } else {
1662 1662 set oldsel $selectedline
1663 1663 }
1664 1664 set didsel 0
1665 1665 set fldtypes {Headline Author Date Committer CDate Comment}
1666 1666 for {set l 0} {$l < $numcommits} {incr l} {
1667 1667 set id $lineid($l)
1668 1668 set info $commitinfo($id)
1669 1669 set doesmatch 0
1670 1670 foreach f $info ty $fldtypes {
1671 1671 if {$findloc != "All fields" && $findloc != $ty} {
1672 1672 continue
1673 1673 }
1674 1674 set matches [findmatches $f]
1675 1675 if {$matches == {}} continue
1676 1676 set doesmatch 1
1677 1677 if {$ty == "Headline"} {
1678 1678 markmatches $canv $l $f $linehtag($l) $matches $mainfont
1679 1679 } elseif {$ty == "Author"} {
1680 1680 markmatches $canv2 $l $f $linentag($l) $matches $namefont
1681 1681 } elseif {$ty == "Date"} {
1682 1682 markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
1683 1683 }
1684 1684 }
1685 1685 if {$doesmatch} {
1686 1686 lappend matchinglines $l
1687 1687 if {!$didsel && $l > $oldsel} {
1688 1688 findselectline $l
1689 1689 set didsel 1
1690 1690 }
1691 1691 }
1692 1692 }
1693 1693 if {$matchinglines == {}} {
1694 1694 bell
1695 1695 } elseif {!$didsel} {
1696 1696 findselectline [lindex $matchinglines 0]
1697 1697 }
1698 1698 }
1699 1699
1700 1700 proc findselectline {l} {
1701 1701 global findloc commentend ctext
1702 1702 selectline $l 1
1703 1703 if {$findloc == "All fields" || $findloc == "Comments"} {
1704 1704 # highlight the matches in the comments
1705 1705 set f [$ctext get 1.0 $commentend]
1706 1706 set matches [findmatches $f]
1707 1707 foreach match $matches {
1708 1708 set start [lindex $match 0]
1709 1709 set end [expr [lindex $match 1] + 1]
1710 1710 $ctext tag add found "1.0 + $start c" "1.0 + $end c"
1711 1711 }
1712 1712 }
1713 1713 }
1714 1714
1715 1715 proc findnext {restart} {
1716 1716 global matchinglines selectedline
1717 1717 if {![info exists matchinglines]} {
1718 1718 if {$restart} {
1719 1719 dofind
1720 1720 }
1721 1721 return
1722 1722 }
1723 1723 if {![info exists selectedline]} return
1724 1724 foreach l $matchinglines {
1725 1725 if {$l > $selectedline} {
1726 1726 findselectline $l
1727 1727 return
1728 1728 }
1729 1729 }
1730 1730 bell
1731 1731 }
1732 1732
1733 1733 proc findprev {} {
1734 1734 global matchinglines selectedline
1735 1735 if {![info exists matchinglines]} {
1736 1736 dofind
1737 1737 return
1738 1738 }
1739 1739 if {![info exists selectedline]} return
1740 1740 set prev {}
1741 1741 foreach l $matchinglines {
1742 1742 if {$l >= $selectedline} break
1743 1743 set prev $l
1744 1744 }
1745 1745 if {$prev != {}} {
1746 1746 findselectline $prev
1747 1747 } else {
1748 1748 bell
1749 1749 }
1750 1750 }
1751 1751
1752 1752 proc findlocchange {name ix op} {
1753 1753 global findloc findtype findtypemenu
1754 1754 if {$findloc == "Pickaxe"} {
1755 1755 set findtype Exact
1756 1756 set state disabled
1757 1757 } else {
1758 1758 set state normal
1759 1759 }
1760 1760 $findtypemenu entryconf 1 -state $state
1761 1761 $findtypemenu entryconf 2 -state $state
1762 1762 }
1763 1763
1764 1764 proc stopfindproc {{done 0}} {
1765 1765 global findprocpid findprocfile findids
1766 1766 global ctext findoldcursor phase maincursor textcursor
1767 1767 global findinprogress
1768 1768
1769 1769 catch {unset findids}
1770 1770 if {[info exists findprocpid]} {
1771 1771 if {!$done} {
1772 1772 catch {exec kill $findprocpid}
1773 1773 }
1774 1774 catch {close $findprocfile}
1775 1775 unset findprocpid
1776 1776 }
1777 1777 if {[info exists findinprogress]} {
1778 1778 unset findinprogress
1779 1779 if {$phase != "incrdraw"} {
1780 1780 . config -cursor $maincursor
1781 1781 settextcursor $textcursor
1782 1782 }
1783 1783 }
1784 1784 }
1785 1785
1786 1786 proc findpatches {} {
1787 1787 global findstring selectedline numcommits
1788 1788 global findprocpid findprocfile
1789 1789 global finddidsel ctext lineid findinprogress
1790 1790 global findinsertpos
1791 1791 global env
1792 1792
1793 1793 if {$numcommits == 0} return
1794 1794
1795 1795 # make a list of all the ids to search, starting at the one
1796 1796 # after the selected line (if any)
1797 1797 if {[info exists selectedline]} {
1798 1798 set l $selectedline
1799 1799 } else {
1800 1800 set l -1
1801 1801 }
1802 1802 set inputids {}
1803 1803 for {set i 0} {$i < $numcommits} {incr i} {
1804 1804 if {[incr l] >= $numcommits} {
1805 1805 set l 0
1806 1806 }
1807 1807 append inputids $lineid($l) "\n"
1808 1808 }
1809 1809
1810 1810 if {[catch {
1811 1811 set f [open [list | $env(HG) --config ui.report_untrusted=false debug-diff-tree --stdin -s -r -S$findstring << $inputids] r]
1812 1812 } err]} {
1813 1813 error_popup "Error starting search process: $err"
1814 1814 return
1815 1815 }
1816 1816
1817 1817 set findinsertpos end
1818 1818 set findprocfile $f
1819 1819 set findprocpid [pid $f]
1820 1820 fconfigure $f -blocking 0
1821 1821 fileevent $f readable readfindproc
1822 1822 set finddidsel 0
1823 1823 . config -cursor watch
1824 1824 settextcursor watch
1825 1825 set findinprogress 1
1826 1826 }
1827 1827
1828 1828 proc readfindproc {} {
1829 1829 global findprocfile finddidsel
1830 1830 global idline matchinglines findinsertpos
1831 1831
1832 1832 set n [gets $findprocfile line]
1833 1833 if {$n < 0} {
1834 1834 if {[eof $findprocfile]} {
1835 1835 stopfindproc 1
1836 1836 if {!$finddidsel} {
1837 1837 bell
1838 1838 }
1839 1839 }
1840 1840 return
1841 1841 }
1842 1842 if {![regexp {^[0-9a-f]{12}} $line id]} {
1843 1843 error_popup "Can't parse git-diff-tree output: $line"
1844 1844 stopfindproc
1845 1845 return
1846 1846 }
1847 1847 if {![info exists idline($id)]} {
1848 1848 puts stderr "spurious id: $id"
1849 1849 return
1850 1850 }
1851 1851 set l $idline($id)
1852 1852 insertmatch $l $id
1853 1853 }
1854 1854
1855 1855 proc insertmatch {l id} {
1856 1856 global matchinglines findinsertpos finddidsel
1857 1857
1858 1858 if {$findinsertpos == "end"} {
1859 1859 if {$matchinglines != {} && $l < [lindex $matchinglines 0]} {
1860 1860 set matchinglines [linsert $matchinglines 0 $l]
1861 1861 set findinsertpos 1
1862 1862 } else {
1863 1863 lappend matchinglines $l
1864 1864 }
1865 1865 } else {
1866 1866 set matchinglines [linsert $matchinglines $findinsertpos $l]
1867 1867 incr findinsertpos
1868 1868 }
1869 1869 markheadline $l $id
1870 1870 if {!$finddidsel} {
1871 1871 findselectline $l
1872 1872 set finddidsel 1
1873 1873 }
1874 1874 }
1875 1875
1876 1876 proc findfiles {} {
1877 1877 global selectedline numcommits lineid ctext
1878 1878 global ffileline finddidsel parents nparents
1879 1879 global findinprogress findstartline findinsertpos
1880 1880 global treediffs fdiffids fdiffsneeded fdiffpos
1881 1881 global findmergefiles
1882 1882 global env
1883 1883
1884 1884 if {$numcommits == 0} return
1885 1885
1886 1886 if {[info exists selectedline]} {
1887 1887 set l [expr {$selectedline + 1}]
1888 1888 } else {
1889 1889 set l 0
1890 1890 }
1891 1891 set ffileline $l
1892 1892 set findstartline $l
1893 1893 set diffsneeded {}
1894 1894 set fdiffsneeded {}
1895 1895 while 1 {
1896 1896 set id $lineid($l)
1897 1897 if {$findmergefiles || $nparents($id) == 1} {
1898 1898 foreach p $parents($id) {
1899 1899 if {![info exists treediffs([list $id $p])]} {
1900 1900 append diffsneeded "$id $p\n"
1901 1901 lappend fdiffsneeded [list $id $p]
1902 1902 }
1903 1903 }
1904 1904 }
1905 1905 if {[incr l] >= $numcommits} {
1906 1906 set l 0
1907 1907 }
1908 1908 if {$l == $findstartline} break
1909 1909 }
1910 1910
1911 1911 # start off a git-diff-tree process if needed
1912 1912 if {$diffsneeded ne {}} {
1913 1913 if {[catch {
1914 1914 set df [open [list | $env(HG) --config ui.report_untrusted=false debug-diff-tree -r --stdin << $diffsneeded] r]
1915 1915 } err ]} {
1916 1916 error_popup "Error starting search process: $err"
1917 1917 return
1918 1918 }
1919 1919 catch {unset fdiffids}
1920 1920 set fdiffpos 0
1921 1921 fconfigure $df -blocking 0
1922 1922 fileevent $df readable [list readfilediffs $df]
1923 1923 }
1924 1924
1925 1925 set finddidsel 0
1926 1926 set findinsertpos end
1927 1927 set id $lineid($l)
1928 1928 set p [lindex $parents($id) 0]
1929 1929 . config -cursor watch
1930 1930 settextcursor watch
1931 1931 set findinprogress 1
1932 1932 findcont [list $id $p]
1933 1933 update
1934 1934 }
1935 1935
1936 1936 proc readfilediffs {df} {
1937 1937 global findids fdiffids fdiffs
1938 1938
1939 1939 set n [gets $df line]
1940 1940 if {$n < 0} {
1941 1941 if {[eof $df]} {
1942 1942 donefilediff
1943 1943 if {[catch {close $df} err]} {
1944 1944 stopfindproc
1945 1945 bell
1946 1946 error_popup "Error in hg debug-diff-tree: $err"
1947 1947 } elseif {[info exists findids]} {
1948 1948 set ids $findids
1949 1949 stopfindproc
1950 1950 bell
1951 1951 error_popup "Couldn't find diffs for {$ids}"
1952 1952 }
1953 1953 }
1954 1954 return
1955 1955 }
1956 1956 if {[regexp {^([0-9a-f]{12}) \(from ([0-9a-f]{12})\)} $line match id p]} {
1957 1957 # start of a new string of diffs
1958 1958 donefilediff
1959 1959 set fdiffids [list $id $p]
1960 1960 set fdiffs {}
1961 1961 } elseif {[string match ":*" $line]} {
1962 1962 lappend fdiffs [lindex $line 5]
1963 1963 }
1964 1964 }
1965 1965
1966 1966 proc donefilediff {} {
1967 1967 global fdiffids fdiffs treediffs findids
1968 1968 global fdiffsneeded fdiffpos
1969 1969
1970 1970 if {[info exists fdiffids]} {
1971 1971 while {[lindex $fdiffsneeded $fdiffpos] ne $fdiffids
1972 1972 && $fdiffpos < [llength $fdiffsneeded]} {
1973 1973 # git-diff-tree doesn't output anything for a commit
1974 1974 # which doesn't change anything
1975 1975 set nullids [lindex $fdiffsneeded $fdiffpos]
1976 1976 set treediffs($nullids) {}
1977 1977 if {[info exists findids] && $nullids eq $findids} {
1978 1978 unset findids
1979 1979 findcont $nullids
1980 1980 }
1981 1981 incr fdiffpos
1982 1982 }
1983 1983 incr fdiffpos
1984 1984
1985 1985 if {![info exists treediffs($fdiffids)]} {
1986 1986 set treediffs($fdiffids) $fdiffs
1987 1987 }
1988 1988 if {[info exists findids] && $fdiffids eq $findids} {
1989 1989 unset findids
1990 1990 findcont $fdiffids
1991 1991 }
1992 1992 }
1993 1993 }
1994 1994
1995 1995 proc findcont {ids} {
1996 1996 global findids treediffs parents nparents
1997 1997 global ffileline findstartline finddidsel
1998 1998 global lineid numcommits matchinglines findinprogress
1999 1999 global findmergefiles
2000 2000
2001 2001 set id [lindex $ids 0]
2002 2002 set p [lindex $ids 1]
2003 2003 set pi [lsearch -exact $parents($id) $p]
2004 2004 set l $ffileline
2005 2005 while 1 {
2006 2006 if {$findmergefiles || $nparents($id) == 1} {
2007 2007 if {![info exists treediffs($ids)]} {
2008 2008 set findids $ids
2009 2009 set ffileline $l
2010 2010 return
2011 2011 }
2012 2012 set doesmatch 0
2013 2013 foreach f $treediffs($ids) {
2014 2014 set x [findmatches $f]
2015 2015 if {$x != {}} {
2016 2016 set doesmatch 1
2017 2017 break
2018 2018 }
2019 2019 }
2020 2020 if {$doesmatch} {
2021 2021 insertmatch $l $id
2022 2022 set pi $nparents($id)
2023 2023 }
2024 2024 } else {
2025 2025 set pi $nparents($id)
2026 2026 }
2027 2027 if {[incr pi] >= $nparents($id)} {
2028 2028 set pi 0
2029 2029 if {[incr l] >= $numcommits} {
2030 2030 set l 0
2031 2031 }
2032 2032 if {$l == $findstartline} break
2033 2033 set id $lineid($l)
2034 2034 }
2035 2035 set p [lindex $parents($id) $pi]
2036 2036 set ids [list $id $p]
2037 2037 }
2038 2038 stopfindproc
2039 2039 if {!$finddidsel} {
2040 2040 bell
2041 2041 }
2042 2042 }
2043 2043
2044 2044 # mark a commit as matching by putting a yellow background
2045 2045 # behind the headline
2046 2046 proc markheadline {l id} {
2047 2047 global canv mainfont linehtag commitinfo
2048 2048
2049 2049 set bbox [$canv bbox $linehtag($l)]
2050 2050 set t [$canv create rect $bbox -outline {} -tags matches -fill yellow]
2051 2051 $canv lower $t
2052 2052 }
2053 2053
2054 2054 # mark the bits of a headline, author or date that match a find string
2055 2055 proc markmatches {canv l str tag matches font} {
2056 2056 set bbox [$canv bbox $tag]
2057 2057 set x0 [lindex $bbox 0]
2058 2058 set y0 [lindex $bbox 1]
2059 2059 set y1 [lindex $bbox 3]
2060 2060 foreach match $matches {
2061 2061 set start [lindex $match 0]
2062 2062 set end [lindex $match 1]
2063 2063 if {$start > $end} continue
2064 2064 set xoff [font measure $font [string range $str 0 [expr $start-1]]]
2065 2065 set xlen [font measure $font [string range $str 0 [expr $end]]]
2066 2066 set t [$canv create rect [expr $x0+$xoff] $y0 [expr $x0+$xlen+2] $y1 \
2067 2067 -outline {} -tags matches -fill yellow]
2068 2068 $canv lower $t
2069 2069 }
2070 2070 }
2071 2071
2072 2072 proc unmarkmatches {} {
2073 2073 global matchinglines findids
2074 2074 allcanvs delete matches
2075 2075 catch {unset matchinglines}
2076 2076 catch {unset findids}
2077 2077 }
2078 2078
2079 2079 proc selcanvline {w x y} {
2080 2080 global canv canvy0 ctext linespc
2081 2081 global lineid linehtag linentag linedtag rowtextx
2082 2082 set ymax [lindex [$canv cget -scrollregion] 3]
2083 2083 if {$ymax == {}} return
2084 2084 set yfrac [lindex [$canv yview] 0]
2085 2085 set y [expr {$y + $yfrac * $ymax}]
2086 2086 set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
2087 2087 if {$l < 0} {
2088 2088 set l 0
2089 2089 }
2090 2090 if {$w eq $canv} {
2091 2091 if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
2092 2092 }
2093 2093 unmarkmatches
2094 2094 selectline $l 1
2095 2095 }
2096 2096
2097 2097 proc commit_descriptor {p} {
2098 2098 global commitinfo
2099 2099 set l "..."
2100 2100 if {[info exists commitinfo($p)]} {
2101 2101 set l [lindex $commitinfo($p) 0]
2102 2102 set r [lindex $commitinfo($p) 6]
2103 2103 }
2104 2104 return "$r:$p ($l)"
2105 2105 }
2106 2106
2107 2107 # append some text to the ctext widget, and make any SHA1 ID
2108 2108 # that we know about be a clickable link.
2109 2109 proc appendwithlinks {text} {
2110 2110 global ctext idline linknum
2111 2111
2112 2112 set start [$ctext index "end - 1c"]
2113 2113 $ctext insert end $text
2114 2114 $ctext insert end "\n"
2115 2115 set links [regexp -indices -all -inline {[0-9a-f]{12}} $text]
2116 2116 foreach l $links {
2117 2117 set s [lindex $l 0]
2118 2118 set e [lindex $l 1]
2119 2119 set linkid [string range $text $s $e]
2120 2120 if {![info exists idline($linkid)]} continue
2121 2121 incr e
2122 2122 $ctext tag add link "$start + $s c" "$start + $e c"
2123 2123 $ctext tag add link$linknum "$start + $s c" "$start + $e c"
2124 2124 $ctext tag bind link$linknum <1> [list selectline $idline($linkid) 1]
2125 2125 incr linknum
2126 2126 }
2127 2127 $ctext tag conf link -foreground blue -underline 1
2128 2128 $ctext tag bind link <Enter> { %W configure -cursor hand2 }
2129 2129 $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
2130 2130 }
2131 2131
2132 2132 proc selectline {l isnew} {
2133 2133 global canv canv2 canv3 ctext commitinfo selectedline
2134 2134 global lineid linehtag linentag linedtag
2135 2135 global canvy0 linespc parents nparents children
2136 2136 global cflist currentid sha1entry
2137 2137 global commentend idtags idline linknum
2138 2138
2139 2139 $canv delete hover
2140 2140 normalline
2141 2141 if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
2142 2142 $canv delete secsel
2143 2143 set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
2144 2144 -tags secsel -fill [$canv cget -selectbackground]]
2145 2145 $canv lower $t
2146 2146 $canv2 delete secsel
2147 2147 set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
2148 2148 -tags secsel -fill [$canv2 cget -selectbackground]]
2149 2149 $canv2 lower $t
2150 2150 $canv3 delete secsel
2151 2151 set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
2152 2152 -tags secsel -fill [$canv3 cget -selectbackground]]
2153 2153 $canv3 lower $t
2154 2154 set y [expr {$canvy0 + $l * $linespc}]
2155 2155 set ymax [lindex [$canv cget -scrollregion] 3]
2156 2156 set ytop [expr {$y - $linespc - 1}]
2157 2157 set ybot [expr {$y + $linespc + 1}]
2158 2158 set wnow [$canv yview]
2159 2159 set wtop [expr [lindex $wnow 0] * $ymax]
2160 2160 set wbot [expr [lindex $wnow 1] * $ymax]
2161 2161 set wh [expr {$wbot - $wtop}]
2162 2162 set newtop $wtop
2163 2163 if {$ytop < $wtop} {
2164 2164 if {$ybot < $wtop} {
2165 2165 set newtop [expr {$y - $wh / 2.0}]
2166 2166 } else {
2167 2167 set newtop $ytop
2168 2168 if {$newtop > $wtop - $linespc} {
2169 2169 set newtop [expr {$wtop - $linespc}]
2170 2170 }
2171 2171 }
2172 2172 } elseif {$ybot > $wbot} {
2173 2173 if {$ytop > $wbot} {
2174 2174 set newtop [expr {$y - $wh / 2.0}]
2175 2175 } else {
2176 2176 set newtop [expr {$ybot - $wh}]
2177 2177 if {$newtop < $wtop + $linespc} {
2178 2178 set newtop [expr {$wtop + $linespc}]
2179 2179 }
2180 2180 }
2181 2181 }
2182 2182 if {$newtop != $wtop} {
2183 2183 if {$newtop < 0} {
2184 2184 set newtop 0
2185 2185 }
2186 2186 allcanvs yview moveto [expr $newtop * 1.0 / $ymax]
2187 2187 }
2188 2188
2189 2189 if {$isnew} {
2190 2190 addtohistory [list selectline $l 0]
2191 2191 }
2192 2192
2193 2193 set selectedline $l
2194 2194
2195 2195 set id $lineid($l)
2196 2196 set currentid $id
2197 2197 $sha1entry delete 0 end
2198 2198 $sha1entry insert 0 $id
2199 2199 $sha1entry selection from 0
2200 2200 $sha1entry selection to end
2201 2201
2202 2202 $ctext conf -state normal
2203 2203 $ctext delete 0.0 end
2204 2204 set linknum 0
2205 2205 $ctext mark set fmark.0 0.0
2206 2206 $ctext mark gravity fmark.0 left
2207 2207 set info $commitinfo($id)
2208 2208 $ctext insert end "Revision: [lindex $info 6]\n"
2209 2209 $ctext insert end "Author: [lindex $info 1] [lindex $info 2]\n"
2210 2210 $ctext insert end "Committer: [lindex $info 3] [lindex $info 4]\n"
2211 2211 if {[info exists idtags($id)]} {
2212 2212 $ctext insert end "Tags:"
2213 2213 foreach tag $idtags($id) {
2214 2214 $ctext insert end " $tag"
2215 2215 }
2216 2216 $ctext insert end "\n"
2217 2217 }
2218 2218
2219 2219 set comment {}
2220 2220 if {[info exists parents($id)]} {
2221 2221 foreach p $parents($id) {
2222 2222 append comment "Parent: [commit_descriptor $p]\n"
2223 2223 }
2224 2224 }
2225 2225 if {[info exists children($id)]} {
2226 2226 foreach c $children($id) {
2227 2227 append comment "Child: [commit_descriptor $c]\n"
2228 2228 }
2229 2229 }
2230 2230 append comment "\n"
2231 2231 append comment [lindex $info 5]
2232 2232
2233 2233 # make anything that looks like a SHA1 ID be a clickable link
2234 2234 appendwithlinks $comment
2235 2235
2236 2236 $ctext tag delete Comments
2237 2237 $ctext tag remove found 1.0 end
2238 2238 $ctext conf -state disabled
2239 2239 set commentend [$ctext index "end - 1c"]
2240 2240
2241 2241 $cflist delete 0 end
2242 2242 $cflist insert end "Comments"
2243 2243 if {$nparents($id) == 1} {
2244 2244 startdiff [concat $id $parents($id)]
2245 2245 } elseif {$nparents($id) > 1} {
2246 2246 mergediff $id
2247 2247 }
2248 2248 }
2249 2249
2250 2250 proc selnextline {dir} {
2251 2251 global selectedline
2252 2252 if {![info exists selectedline]} return
2253 2253 set l [expr $selectedline + $dir]
2254 2254 unmarkmatches
2255 2255 selectline $l 1
2256 2256 }
2257 2257
2258 2258 proc unselectline {} {
2259 2259 global selectedline
2260 2260
2261 2261 catch {unset selectedline}
2262 2262 allcanvs delete secsel
2263 2263 }
2264 2264
2265 2265 proc addtohistory {cmd} {
2266 2266 global history historyindex
2267 2267
2268 2268 if {$historyindex > 0
2269 2269 && [lindex $history [expr {$historyindex - 1}]] == $cmd} {
2270 2270 return
2271 2271 }
2272 2272
2273 2273 if {$historyindex < [llength $history]} {
2274 2274 set history [lreplace $history $historyindex end $cmd]
2275 2275 } else {
2276 2276 lappend history $cmd
2277 2277 }
2278 2278 incr historyindex
2279 2279 if {$historyindex > 1} {
2280 2280 .ctop.top.bar.leftbut conf -state normal
2281 2281 } else {
2282 2282 .ctop.top.bar.leftbut conf -state disabled
2283 2283 }
2284 2284 .ctop.top.bar.rightbut conf -state disabled
2285 2285 }
2286 2286
2287 2287 proc goback {} {
2288 2288 global history historyindex
2289 2289
2290 2290 if {$historyindex > 1} {
2291 2291 incr historyindex -1
2292 2292 set cmd [lindex $history [expr {$historyindex - 1}]]
2293 2293 eval $cmd
2294 2294 .ctop.top.bar.rightbut conf -state normal
2295 2295 }
2296 2296 if {$historyindex <= 1} {
2297 2297 .ctop.top.bar.leftbut conf -state disabled
2298 2298 }
2299 2299 }
2300 2300
2301 2301 proc goforw {} {
2302 2302 global history historyindex
2303 2303
2304 2304 if {$historyindex < [llength $history]} {
2305 2305 set cmd [lindex $history $historyindex]
2306 2306 incr historyindex
2307 2307 eval $cmd
2308 2308 .ctop.top.bar.leftbut conf -state normal
2309 2309 }
2310 2310 if {$historyindex >= [llength $history]} {
2311 2311 .ctop.top.bar.rightbut conf -state disabled
2312 2312 }
2313 2313 }
2314 2314
2315 2315 proc mergediff {id} {
2316 2316 global parents diffmergeid diffmergegca mergefilelist diffpindex
2317 2317
2318 2318 set diffmergeid $id
2319 2319 set diffpindex -1
2320 2320 set diffmergegca [findgca $parents($id)]
2321 2321 if {[info exists mergefilelist($id)]} {
2322 2322 if {$mergefilelist($id) ne {}} {
2323 2323 showmergediff
2324 2324 }
2325 2325 } else {
2326 2326 contmergediff {}
2327 2327 }
2328 2328 }
2329 2329
2330 2330 proc findgca {ids} {
2331 2331 global env
2332 2332 set gca {}
2333 2333 foreach id $ids {
2334 2334 if {$gca eq {}} {
2335 2335 set gca $id
2336 2336 } else {
2337 2337 if {[catch {
2338 2338 set gca [exec $env(HG) --config ui.report_untrusted=false debug-merge-base $gca $id]
2339 2339 } err]} {
2340 2340 return {}
2341 2341 }
2342 2342 }
2343 2343 }
2344 2344 return $gca
2345 2345 }
2346 2346
2347 2347 proc contmergediff {ids} {
2348 2348 global diffmergeid diffpindex parents nparents diffmergegca
2349 2349 global treediffs mergefilelist diffids treepending
2350 2350
2351 2351 # diff the child against each of the parents, and diff
2352 2352 # each of the parents against the GCA.
2353 2353 while 1 {
2354 2354 if {[lindex $ids 0] == $diffmergeid && $diffmergegca ne {}} {
2355 2355 set ids [list [lindex $ids 1] $diffmergegca]
2356 2356 } else {
2357 2357 if {[incr diffpindex] >= $nparents($diffmergeid)} break
2358 2358 set p [lindex $parents($diffmergeid) $diffpindex]
2359 2359 set ids [list $diffmergeid $p]
2360 2360 }
2361 2361 if {![info exists treediffs($ids)]} {
2362 2362 set diffids $ids
2363 2363 if {![info exists treepending]} {
2364 2364 gettreediffs $ids
2365 2365 }
2366 2366 return
2367 2367 }
2368 2368 }
2369 2369
2370 2370 # If a file in some parent is different from the child and also
2371 2371 # different from the GCA, then it's interesting.
2372 2372 # If we don't have a GCA, then a file is interesting if it is
2373 2373 # different from the child in all the parents.
2374 2374 if {$diffmergegca ne {}} {
2375 2375 set files {}
2376 2376 foreach p $parents($diffmergeid) {
2377 2377 set gcadiffs $treediffs([list $p $diffmergegca])
2378 2378 foreach f $treediffs([list $diffmergeid $p]) {
2379 2379 if {[lsearch -exact $files $f] < 0
2380 2380 && [lsearch -exact $gcadiffs $f] >= 0} {
2381 2381 lappend files $f
2382 2382 }
2383 2383 }
2384 2384 }
2385 2385 set files [lsort $files]
2386 2386 } else {
2387 2387 set p [lindex $parents($diffmergeid) 0]
2388 2388 set files $treediffs([list $diffmergeid $p])
2389 2389 for {set i 1} {$i < $nparents($diffmergeid) && $files ne {}} {incr i} {
2390 2390 set p [lindex $parents($diffmergeid) $i]
2391 2391 set df $treediffs([list $diffmergeid $p])
2392 2392 set nf {}
2393 2393 foreach f $files {
2394 2394 if {[lsearch -exact $df $f] >= 0} {
2395 2395 lappend nf $f
2396 2396 }
2397 2397 }
2398 2398 set files $nf
2399 2399 }
2400 2400 }
2401 2401
2402 2402 set mergefilelist($diffmergeid) $files
2403 2403 if {$files ne {}} {
2404 2404 showmergediff
2405 2405 }
2406 2406 }
2407 2407
2408 2408 proc showmergediff {} {
2409 2409 global cflist diffmergeid mergefilelist parents
2410 2410 global diffopts diffinhunk currentfile currenthunk filelines
2411 2411 global diffblocked groupfilelast mergefds groupfilenum grouphunks
2412 2412 global env
2413 2413
2414 2414 set files $mergefilelist($diffmergeid)
2415 2415 foreach f $files {
2416 2416 $cflist insert end $f
2417 2417 }
2418 2418 set env(GIT_DIFF_OPTS) $diffopts
2419 2419 set flist {}
2420 2420 catch {unset currentfile}
2421 2421 catch {unset currenthunk}
2422 2422 catch {unset filelines}
2423 2423 catch {unset groupfilenum}
2424 2424 catch {unset grouphunks}
2425 2425 set groupfilelast -1
2426 2426 foreach p $parents($diffmergeid) {
2427 2427 set cmd [list | $env(HG) --config ui.report_untrusted=false debug-diff-tree -p $p $diffmergeid]
2428 2428 set cmd [concat $cmd $mergefilelist($diffmergeid)]
2429 2429 if {[catch {set f [open $cmd r]} err]} {
2430 2430 error_popup "Error getting diffs: $err"
2431 2431 foreach f $flist {
2432 2432 catch {close $f}
2433 2433 }
2434 2434 return
2435 2435 }
2436 2436 lappend flist $f
2437 2437 set ids [list $diffmergeid $p]
2438 2438 set mergefds($ids) $f
2439 2439 set diffinhunk($ids) 0
2440 2440 set diffblocked($ids) 0
2441 2441 fconfigure $f -blocking 0
2442 2442 fileevent $f readable [list getmergediffline $f $ids $diffmergeid]
2443 2443 }
2444 2444 }
2445 2445
2446 2446 proc getmergediffline {f ids id} {
2447 2447 global diffmergeid diffinhunk diffoldlines diffnewlines
2448 2448 global currentfile currenthunk
2449 2449 global diffoldstart diffnewstart diffoldlno diffnewlno
2450 2450 global diffblocked mergefilelist
2451 2451 global noldlines nnewlines difflcounts filelines
2452 2452
2453 2453 set n [gets $f line]
2454 2454 if {$n < 0} {
2455 2455 if {![eof $f]} return
2456 2456 }
2457 2457
2458 2458 if {!([info exists diffmergeid] && $diffmergeid == $id)} {
2459 2459 if {$n < 0} {
2460 2460 close $f
2461 2461 }
2462 2462 return
2463 2463 }
2464 2464
2465 2465 if {$diffinhunk($ids) != 0} {
2466 2466 set fi $currentfile($ids)
2467 2467 if {$n > 0 && [regexp {^[-+ \\]} $line match]} {
2468 2468 # continuing an existing hunk
2469 2469 set line [string range $line 1 end]
2470 2470 set p [lindex $ids 1]
2471 2471 if {$match eq "-" || $match eq " "} {
2472 2472 set filelines($p,$fi,$diffoldlno($ids)) $line
2473 2473 incr diffoldlno($ids)
2474 2474 }
2475 2475 if {$match eq "+" || $match eq " "} {
2476 2476 set filelines($id,$fi,$diffnewlno($ids)) $line
2477 2477 incr diffnewlno($ids)
2478 2478 }
2479 2479 if {$match eq " "} {
2480 2480 if {$diffinhunk($ids) == 2} {
2481 2481 lappend difflcounts($ids) \
2482 2482 [list $noldlines($ids) $nnewlines($ids)]
2483 2483 set noldlines($ids) 0
2484 2484 set diffinhunk($ids) 1
2485 2485 }
2486 2486 incr noldlines($ids)
2487 2487 } elseif {$match eq "-" || $match eq "+"} {
2488 2488 if {$diffinhunk($ids) == 1} {
2489 2489 lappend difflcounts($ids) [list $noldlines($ids)]
2490 2490 set noldlines($ids) 0
2491 2491 set nnewlines($ids) 0
2492 2492 set diffinhunk($ids) 2
2493 2493 }
2494 2494 if {$match eq "-"} {
2495 2495 incr noldlines($ids)
2496 2496 } else {
2497 2497 incr nnewlines($ids)
2498 2498 }
2499 2499 }
2500 2500 # and if it's \ No newline at end of line, then what?
2501 2501 return
2502 2502 }
2503 2503 # end of a hunk
2504 2504 if {$diffinhunk($ids) == 1 && $noldlines($ids) != 0} {
2505 2505 lappend difflcounts($ids) [list $noldlines($ids)]
2506 2506 } elseif {$diffinhunk($ids) == 2
2507 2507 && ($noldlines($ids) != 0 || $nnewlines($ids) != 0)} {
2508 2508 lappend difflcounts($ids) [list $noldlines($ids) $nnewlines($ids)]
2509 2509 }
2510 2510 set currenthunk($ids) [list $currentfile($ids) \
2511 2511 $diffoldstart($ids) $diffnewstart($ids) \
2512 2512 $diffoldlno($ids) $diffnewlno($ids) \
2513 2513 $difflcounts($ids)]
2514 2514 set diffinhunk($ids) 0
2515 2515 # -1 = need to block, 0 = unblocked, 1 = is blocked
2516 2516 set diffblocked($ids) -1
2517 2517 processhunks
2518 2518 if {$diffblocked($ids) == -1} {
2519 2519 fileevent $f readable {}
2520 2520 set diffblocked($ids) 1
2521 2521 }
2522 2522 }
2523 2523
2524 2524 if {$n < 0} {
2525 2525 # eof
2526 2526 if {!$diffblocked($ids)} {
2527 2527 close $f
2528 2528 set currentfile($ids) [llength $mergefilelist($diffmergeid)]
2529 2529 set currenthunk($ids) [list $currentfile($ids) 0 0 0 0 {}]
2530 2530 processhunks
2531 2531 }
2532 2532 } elseif {[regexp {^diff --git a/(.*) b/} $line match fname]} {
2533 2533 # start of a new file
2534 2534 set currentfile($ids) \
2535 2535 [lsearch -exact $mergefilelist($diffmergeid) $fname]
2536 2536 } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2537 2537 $line match f1l f1c f2l f2c rest]} {
2538 2538 if {[info exists currentfile($ids)] && $currentfile($ids) >= 0} {
2539 2539 # start of a new hunk
2540 2540 if {$f1l == 0 && $f1c == 0} {
2541 2541 set f1l 1
2542 2542 }
2543 2543 if {$f2l == 0 && $f2c == 0} {
2544 2544 set f2l 1
2545 2545 }
2546 2546 set diffinhunk($ids) 1
2547 2547 set diffoldstart($ids) $f1l
2548 2548 set diffnewstart($ids) $f2l
2549 2549 set diffoldlno($ids) $f1l
2550 2550 set diffnewlno($ids) $f2l
2551 2551 set difflcounts($ids) {}
2552 2552 set noldlines($ids) 0
2553 2553 set nnewlines($ids) 0
2554 2554 }
2555 2555 }
2556 2556 }
2557 2557
2558 2558 proc processhunks {} {
2559 2559 global diffmergeid parents nparents currenthunk
2560 2560 global mergefilelist diffblocked mergefds
2561 2561 global grouphunks grouplinestart grouplineend groupfilenum
2562 2562
2563 2563 set nfiles [llength $mergefilelist($diffmergeid)]
2564 2564 while 1 {
2565 2565 set fi $nfiles
2566 2566 set lno 0
2567 2567 # look for the earliest hunk
2568 2568 foreach p $parents($diffmergeid) {
2569 2569 set ids [list $diffmergeid $p]
2570 2570 if {![info exists currenthunk($ids)]} return
2571 2571 set i [lindex $currenthunk($ids) 0]
2572 2572 set l [lindex $currenthunk($ids) 2]
2573 2573 if {$i < $fi || ($i == $fi && $l < $lno)} {
2574 2574 set fi $i
2575 2575 set lno $l
2576 2576 set pi $p
2577 2577 }
2578 2578 }
2579 2579
2580 2580 if {$fi < $nfiles} {
2581 2581 set ids [list $diffmergeid $pi]
2582 2582 set hunk $currenthunk($ids)
2583 2583 unset currenthunk($ids)
2584 2584 if {$diffblocked($ids) > 0} {
2585 2585 fileevent $mergefds($ids) readable \
2586 2586 [list getmergediffline $mergefds($ids) $ids $diffmergeid]
2587 2587 }
2588 2588 set diffblocked($ids) 0
2589 2589
2590 2590 if {[info exists groupfilenum] && $groupfilenum == $fi
2591 2591 && $lno <= $grouplineend} {
2592 2592 # add this hunk to the pending group
2593 2593 lappend grouphunks($pi) $hunk
2594 2594 set endln [lindex $hunk 4]
2595 2595 if {$endln > $grouplineend} {
2596 2596 set grouplineend $endln
2597 2597 }
2598 2598 continue
2599 2599 }
2600 2600 }
2601 2601
2602 2602 # succeeding stuff doesn't belong in this group, so
2603 2603 # process the group now
2604 2604 if {[info exists groupfilenum]} {
2605 2605 processgroup
2606 2606 unset groupfilenum
2607 2607 unset grouphunks
2608 2608 }
2609 2609
2610 2610 if {$fi >= $nfiles} break
2611 2611
2612 2612 # start a new group
2613 2613 set groupfilenum $fi
2614 2614 set grouphunks($pi) [list $hunk]
2615 2615 set grouplinestart $lno
2616 2616 set grouplineend [lindex $hunk 4]
2617 2617 }
2618 2618 }
2619 2619
2620 2620 proc processgroup {} {
2621 2621 global groupfilelast groupfilenum difffilestart
2622 2622 global mergefilelist diffmergeid ctext filelines
2623 2623 global parents diffmergeid diffoffset
2624 2624 global grouphunks grouplinestart grouplineend nparents
2625 2625 global mergemax
2626 2626
2627 2627 $ctext conf -state normal
2628 2628 set id $diffmergeid
2629 2629 set f $groupfilenum
2630 2630 if {$groupfilelast != $f} {
2631 2631 $ctext insert end "\n"
2632 2632 set here [$ctext index "end - 1c"]
2633 2633 set difffilestart($f) $here
2634 2634 set mark fmark.[expr {$f + 1}]
2635 2635 $ctext mark set $mark $here
2636 2636 $ctext mark gravity $mark left
2637 2637 set header [lindex $mergefilelist($id) $f]
2638 2638 set l [expr {(78 - [string length $header]) / 2}]
2639 2639 set pad [string range "----------------------------------------" 1 $l]
2640 2640 $ctext insert end "$pad $header $pad\n" filesep
2641 2641 set groupfilelast $f
2642 2642 foreach p $parents($id) {
2643 2643 set diffoffset($p) 0
2644 2644 }
2645 2645 }
2646 2646
2647 2647 $ctext insert end "@@" msep
2648 2648 set nlines [expr {$grouplineend - $grouplinestart}]
2649 2649 set events {}
2650 2650 set pnum 0
2651 2651 foreach p $parents($id) {
2652 2652 set startline [expr {$grouplinestart + $diffoffset($p)}]
2653 2653 set ol $startline
2654 2654 set nl $grouplinestart
2655 2655 if {[info exists grouphunks($p)]} {
2656 2656 foreach h $grouphunks($p) {
2657 2657 set l [lindex $h 2]
2658 2658 if {$nl < $l} {
2659 2659 for {} {$nl < $l} {incr nl} {
2660 2660 set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2661 2661 incr ol
2662 2662 }
2663 2663 }
2664 2664 foreach chunk [lindex $h 5] {
2665 2665 if {[llength $chunk] == 2} {
2666 2666 set olc [lindex $chunk 0]
2667 2667 set nlc [lindex $chunk 1]
2668 2668 set nnl [expr {$nl + $nlc}]
2669 2669 lappend events [list $nl $nnl $pnum $olc $nlc]
2670 2670 incr ol $olc
2671 2671 set nl $nnl
2672 2672 } else {
2673 2673 incr ol [lindex $chunk 0]
2674 2674 incr nl [lindex $chunk 0]
2675 2675 }
2676 2676 }
2677 2677 }
2678 2678 }
2679 2679 if {$nl < $grouplineend} {
2680 2680 for {} {$nl < $grouplineend} {incr nl} {
2681 2681 set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2682 2682 incr ol
2683 2683 }
2684 2684 }
2685 2685 set nlines [expr {$ol - $startline}]
2686 2686 $ctext insert end " -$startline,$nlines" msep
2687 2687 incr pnum
2688 2688 }
2689 2689
2690 2690 set nlines [expr {$grouplineend - $grouplinestart}]
2691 2691 $ctext insert end " +$grouplinestart,$nlines @@\n" msep
2692 2692
2693 2693 set events [lsort -integer -index 0 $events]
2694 2694 set nevents [llength $events]
2695 2695 set nmerge $nparents($diffmergeid)
2696 2696 set l $grouplinestart
2697 2697 for {set i 0} {$i < $nevents} {set i $j} {
2698 2698 set nl [lindex $events $i 0]
2699 2699 while {$l < $nl} {
2700 2700 $ctext insert end " $filelines($id,$f,$l)\n"
2701 2701 incr l
2702 2702 }
2703 2703 set e [lindex $events $i]
2704 2704 set enl [lindex $e 1]
2705 2705 set j $i
2706 2706 set active {}
2707 2707 while 1 {
2708 2708 set pnum [lindex $e 2]
2709 2709 set olc [lindex $e 3]
2710 2710 set nlc [lindex $e 4]
2711 2711 if {![info exists delta($pnum)]} {
2712 2712 set delta($pnum) [expr {$olc - $nlc}]
2713 2713 lappend active $pnum
2714 2714 } else {
2715 2715 incr delta($pnum) [expr {$olc - $nlc}]
2716 2716 }
2717 2717 if {[incr j] >= $nevents} break
2718 2718 set e [lindex $events $j]
2719 2719 if {[lindex $e 0] >= $enl} break
2720 2720 if {[lindex $e 1] > $enl} {
2721 2721 set enl [lindex $e 1]
2722 2722 }
2723 2723 }
2724 2724 set nlc [expr {$enl - $l}]
2725 2725 set ncol mresult
2726 2726 set bestpn -1
2727 2727 if {[llength $active] == $nmerge - 1} {
2728 2728 # no diff for one of the parents, i.e. it's identical
2729 2729 for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2730 2730 if {![info exists delta($pnum)]} {
2731 2731 if {$pnum < $mergemax} {
2732 2732 lappend ncol m$pnum
2733 2733 } else {
2734 2734 lappend ncol mmax
2735 2735 }
2736 2736 break
2737 2737 }
2738 2738 }
2739 2739 } elseif {[llength $active] == $nmerge} {
2740 2740 # all parents are different, see if one is very similar
2741 2741 set bestsim 30
2742 2742 for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2743 2743 set sim [similarity $pnum $l $nlc $f \
2744 2744 [lrange $events $i [expr {$j-1}]]]
2745 2745 if {$sim > $bestsim} {
2746 2746 set bestsim $sim
2747 2747 set bestpn $pnum
2748 2748 }
2749 2749 }
2750 2750 if {$bestpn >= 0} {
2751 2751 lappend ncol m$bestpn
2752 2752 }
2753 2753 }
2754 2754 set pnum -1
2755 2755 foreach p $parents($id) {
2756 2756 incr pnum
2757 2757 if {![info exists delta($pnum)] || $pnum == $bestpn} continue
2758 2758 set olc [expr {$nlc + $delta($pnum)}]
2759 2759 set ol [expr {$l + $diffoffset($p)}]
2760 2760 incr diffoffset($p) $delta($pnum)
2761 2761 unset delta($pnum)
2762 2762 for {} {$olc > 0} {incr olc -1} {
2763 2763 $ctext insert end "-$filelines($p,$f,$ol)\n" m$pnum
2764 2764 incr ol
2765 2765 }
2766 2766 }
2767 2767 set endl [expr {$l + $nlc}]
2768 2768 if {$bestpn >= 0} {
2769 2769 # show this pretty much as a normal diff
2770 2770 set p [lindex $parents($id) $bestpn]
2771 2771 set ol [expr {$l + $diffoffset($p)}]
2772 2772 incr diffoffset($p) $delta($bestpn)
2773 2773 unset delta($bestpn)
2774 2774 for {set k $i} {$k < $j} {incr k} {
2775 2775 set e [lindex $events $k]
2776 2776 if {[lindex $e 2] != $bestpn} continue
2777 2777 set nl [lindex $e 0]
2778 2778 set ol [expr {$ol + $nl - $l}]
2779 2779 for {} {$l < $nl} {incr l} {
2780 2780 $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2781 2781 }
2782 2782 set c [lindex $e 3]
2783 2783 for {} {$c > 0} {incr c -1} {
2784 2784 $ctext insert end "-$filelines($p,$f,$ol)\n" m$bestpn
2785 2785 incr ol
2786 2786 }
2787 2787 set nl [lindex $e 1]
2788 2788 for {} {$l < $nl} {incr l} {
2789 2789 $ctext insert end "+$filelines($id,$f,$l)\n" mresult
2790 2790 }
2791 2791 }
2792 2792 }
2793 2793 for {} {$l < $endl} {incr l} {
2794 2794 $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2795 2795 }
2796 2796 }
2797 2797 while {$l < $grouplineend} {
2798 2798 $ctext insert end " $filelines($id,$f,$l)\n"
2799 2799 incr l
2800 2800 }
2801 2801 $ctext conf -state disabled
2802 2802 }
2803 2803
2804 2804 proc similarity {pnum l nlc f events} {
2805 2805 global diffmergeid parents diffoffset filelines
2806 2806
2807 2807 set id $diffmergeid
2808 2808 set p [lindex $parents($id) $pnum]
2809 2809 set ol [expr {$l + $diffoffset($p)}]
2810 2810 set endl [expr {$l + $nlc}]
2811 2811 set same 0
2812 2812 set diff 0
2813 2813 foreach e $events {
2814 2814 if {[lindex $e 2] != $pnum} continue
2815 2815 set nl [lindex $e 0]
2816 2816 set ol [expr {$ol + $nl - $l}]
2817 2817 for {} {$l < $nl} {incr l} {
2818 2818 incr same [string length $filelines($id,$f,$l)]
2819 2819 incr same
2820 2820 }
2821 2821 set oc [lindex $e 3]
2822 2822 for {} {$oc > 0} {incr oc -1} {
2823 2823 incr diff [string length $filelines($p,$f,$ol)]
2824 2824 incr diff
2825 2825 incr ol
2826 2826 }
2827 2827 set nl [lindex $e 1]
2828 2828 for {} {$l < $nl} {incr l} {
2829 2829 incr diff [string length $filelines($id,$f,$l)]
2830 2830 incr diff
2831 2831 }
2832 2832 }
2833 2833 for {} {$l < $endl} {incr l} {
2834 2834 incr same [string length $filelines($id,$f,$l)]
2835 2835 incr same
2836 2836 }
2837 2837 if {$same == 0} {
2838 2838 return 0
2839 2839 }
2840 2840 return [expr {200 * $same / (2 * $same + $diff)}]
2841 2841 }
2842 2842
2843 2843 proc startdiff {ids} {
2844 2844 global treediffs diffids treepending diffmergeid
2845 2845
2846 2846 set diffids $ids
2847 2847 catch {unset diffmergeid}
2848 2848 if {![info exists treediffs($ids)]} {
2849 2849 if {![info exists treepending]} {
2850 2850 gettreediffs $ids
2851 2851 }
2852 2852 } else {
2853 2853 addtocflist $ids
2854 2854 }
2855 2855 }
2856 2856
2857 2857 proc addtocflist {ids} {
2858 2858 global treediffs cflist
2859 2859 foreach f $treediffs($ids) {
2860 2860 $cflist insert end $f
2861 2861 }
2862 2862 getblobdiffs $ids
2863 2863 }
2864 2864
2865 2865 proc gettreediffs {ids} {
2866 2866 global treediff parents treepending env
2867 2867 set treepending $ids
2868 2868 set treediff {}
2869 2869 set id [lindex $ids 0]
2870 2870 set p [lindex $ids 1]
2871 2871 if [catch {set gdtf [open "|{$env(HG)} --config ui.report_untrusted=false debug-diff-tree -r $p $id" r]}] return
2872 2872 fconfigure $gdtf -blocking 0
2873 2873 fileevent $gdtf readable [list gettreediffline $gdtf $ids]
2874 2874 }
2875 2875
2876 2876 proc gettreediffline {gdtf ids} {
2877 2877 global treediff treediffs treepending diffids diffmergeid
2878 2878
2879 2879 set n [gets $gdtf line]
2880 2880 if {$n < 0} {
2881 2881 if {![eof $gdtf]} return
2882 2882 close $gdtf
2883 2883 set treediffs($ids) $treediff
2884 2884 unset treepending
2885 2885 if {$ids != $diffids} {
2886 2886 gettreediffs $diffids
2887 2887 } else {
2888 2888 if {[info exists diffmergeid]} {
2889 2889 contmergediff $ids
2890 2890 } else {
2891 2891 addtocflist $ids
2892 2892 }
2893 2893 }
2894 2894 return
2895 2895 }
2896 2896 set tab1 [expr [string first "\t" $line] + 1]
2897 2897 set tab2 [expr [string first "\t" $line $tab1] - 1]
2898 2898 set file [string range $line $tab1 $tab2]
2899 2899 lappend treediff $file
2900 2900 }
2901 2901
2902 2902 proc getblobdiffs {ids} {
2903 2903 global diffopts blobdifffd diffids env curdifftag curtagstart
2904 2904 global difffilestart nextupdate diffinhdr treediffs
2905 2905
2906 2906 set id [lindex $ids 0]
2907 2907 set p [lindex $ids 1]
2908 2908 set env(GIT_DIFF_OPTS) $diffopts
2909 2909 set cmd [list | $env(HG) --config ui.report_untrusted=false debug-diff-tree -r -p -C $p $id]
2910 2910 if {[catch {set bdf [open $cmd r]} err]} {
2911 2911 puts "error getting diffs: $err"
2912 2912 return
2913 2913 }
2914 2914 set diffinhdr 0
2915 2915 fconfigure $bdf -blocking 0
2916 2916 set blobdifffd($ids) $bdf
2917 2917 set curdifftag Comments
2918 2918 set curtagstart 0.0
2919 2919 catch {unset difffilestart}
2920 2920 fileevent $bdf readable [list getblobdiffline $bdf $diffids]
2921 2921 set nextupdate [expr {[clock clicks -milliseconds] + 100}]
2922 2922 }
2923 2923
2924 2924 proc getblobdiffline {bdf ids} {
2925 2925 global diffids blobdifffd ctext curdifftag curtagstart
2926 2926 global diffnexthead diffnextnote difffilestart
2927 2927 global nextupdate diffinhdr treediffs
2928 2928 global gaudydiff
2929 2929
2930 2930 set n [gets $bdf line]
2931 2931 if {$n < 0} {
2932 2932 if {[eof $bdf]} {
2933 2933 close $bdf
2934 2934 if {$ids == $diffids && $bdf == $blobdifffd($ids)} {
2935 2935 $ctext tag add $curdifftag $curtagstart end
2936 2936 }
2937 2937 }
2938 2938 return
2939 2939 }
2940 2940 if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
2941 2941 return
2942 2942 }
2943 2943 regsub -all "\r" $line "" line
2944 2944 $ctext conf -state normal
2945 2945 if {[regexp {^diff --git a/(.*) b/(.*)} $line match fname newname]} {
2946 2946 # start of a new file
2947 2947 $ctext insert end "\n"
2948 2948 $ctext tag add $curdifftag $curtagstart end
2949 2949 set curtagstart [$ctext index "end - 1c"]
2950 2950 set header $newname
2951 2951 set here [$ctext index "end - 1c"]
2952 2952 set i [lsearch -exact $treediffs($diffids) $fname]
2953 2953 if {$i >= 0} {
2954 2954 set difffilestart($i) $here
2955 2955 incr i
2956 2956 $ctext mark set fmark.$i $here
2957 2957 $ctext mark gravity fmark.$i left
2958 2958 }
2959 2959 if {$newname != $fname} {
2960 2960 set i [lsearch -exact $treediffs($diffids) $newname]
2961 2961 if {$i >= 0} {
2962 2962 set difffilestart($i) $here
2963 2963 incr i
2964 2964 $ctext mark set fmark.$i $here
2965 2965 $ctext mark gravity fmark.$i left
2966 2966 }
2967 2967 }
2968 2968 set curdifftag "f:$fname"
2969 2969 $ctext tag delete $curdifftag
2970 2970 set l [expr {(78 - [string length $header]) / 2}]
2971 2971 set pad [string range "----------------------------------------" 1 $l]
2972 2972 $ctext insert end "$pad $header $pad\n" filesep
2973 2973 set diffinhdr 1
2974 2974 } elseif {[regexp {^(---|\+\+\+)} $line]} {
2975 2975 set diffinhdr 0
2976 2976 } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2977 2977 $line match f1l f1c f2l f2c rest]} {
2978 2978 if {$gaudydiff} {
2979 2979 $ctext insert end "\t" hunksep
2980 2980 $ctext insert end " $f1l " d0 " $f2l " d1
2981 2981 $ctext insert end " $rest \n" hunksep
2982 2982 } else {
2983 2983 $ctext insert end "$line\n" hunksep
2984 2984 }
2985 2985 set diffinhdr 0
2986 2986 } else {
2987 2987 set x [string range $line 0 0]
2988 2988 if {$x == "-" || $x == "+"} {
2989 2989 set tag [expr {$x == "+"}]
2990 2990 if {$gaudydiff} {
2991 2991 set line [string range $line 1 end]
2992 2992 }
2993 2993 $ctext insert end "$line\n" d$tag
2994 2994 } elseif {$x == " "} {
2995 2995 if {$gaudydiff} {
2996 2996 set line [string range $line 1 end]
2997 2997 }
2998 2998 $ctext insert end "$line\n"
2999 2999 } elseif {$diffinhdr || $x == "\\"} {
3000 3000 # e.g. "\ No newline at end of file"
3001 3001 $ctext insert end "$line\n" filesep
3002 3002 } elseif {$line != ""} {
3003 3003 # Something else we don't recognize
3004 3004 if {$curdifftag != "Comments"} {
3005 3005 $ctext insert end "\n"
3006 3006 $ctext tag add $curdifftag $curtagstart end
3007 3007 set curtagstart [$ctext index "end - 1c"]
3008 3008 set curdifftag Comments
3009 3009 }
3010 3010 $ctext insert end "$line\n" filesep
3011 3011 }
3012 3012 }
3013 3013 $ctext conf -state disabled
3014 3014 if {[clock clicks -milliseconds] >= $nextupdate} {
3015 3015 incr nextupdate 100
3016 3016 fileevent $bdf readable {}
3017 3017 update
3018 3018 fileevent $bdf readable "getblobdiffline $bdf {$ids}"
3019 3019 }
3020 3020 }
3021 3021
3022 3022 proc nextfile {} {
3023 3023 global difffilestart ctext
3024 3024 set here [$ctext index @0,0]
3025 3025 for {set i 0} {[info exists difffilestart($i)]} {incr i} {
3026 3026 if {[$ctext compare $difffilestart($i) > $here]} {
3027 3027 if {![info exists pos]
3028 3028 || [$ctext compare $difffilestart($i) < $pos]} {
3029 3029 set pos $difffilestart($i)
3030 3030 }
3031 3031 }
3032 3032 }
3033 3033 if {[info exists pos]} {
3034 3034 $ctext yview $pos
3035 3035 }
3036 3036 }
3037 3037
3038 3038 proc listboxsel {} {
3039 3039 global ctext cflist currentid
3040 3040 if {![info exists currentid]} return
3041 3041 set sel [lsort [$cflist curselection]]
3042 3042 if {$sel eq {}} return
3043 3043 set first [lindex $sel 0]
3044 3044 catch {$ctext yview fmark.$first}
3045 3045 }
3046 3046
3047 3047 proc setcoords {} {
3048 3048 global linespc charspc canvx0 canvy0 mainfont
3049 3049 global xspc1 xspc2 lthickness
3050 3050
3051 3051 set linespc [font metrics $mainfont -linespace]
3052 3052 set charspc [font measure $mainfont "m"]
3053 3053 set canvy0 [expr 3 + 0.5 * $linespc]
3054 3054 set canvx0 [expr 3 + 0.5 * $linespc]
3055 3055 set lthickness [expr {int($linespc / 9) + 1}]
3056 3056 set xspc1(0) $linespc
3057 3057 set xspc2 $linespc
3058 3058 }
3059 3059
3060 3060 proc redisplay {} {
3061 3061 global stopped redisplaying phase
3062 3062 if {$stopped > 1} return
3063 3063 if {$phase == "getcommits"} return
3064 3064 set redisplaying 1
3065 3065 if {$phase == "drawgraph" || $phase == "incrdraw"} {
3066 3066 set stopped 1
3067 3067 } else {
3068 3068 drawgraph
3069 3069 }
3070 3070 }
3071 3071
3072 3072 proc incrfont {inc} {
3073 3073 global mainfont namefont textfont ctext canv phase
3074 3074 global stopped entries
3075 3075 unmarkmatches
3076 3076 set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
3077 3077 set namefont [lreplace $namefont 1 1 [expr {[lindex $namefont 1] + $inc}]]
3078 3078 set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
3079 3079 setcoords
3080 3080 $ctext conf -font $textfont
3081 3081 $ctext tag conf filesep -font [concat $textfont bold]
3082 3082 foreach e $entries {
3083 3083 $e conf -font $mainfont
3084 3084 }
3085 3085 if {$phase == "getcommits"} {
3086 3086 $canv itemconf textitems -font $mainfont
3087 3087 }
3088 3088 redisplay
3089 3089 }
3090 3090
3091 3091 proc clearsha1 {} {
3092 3092 global sha1entry sha1string
3093 3093 if {[string length $sha1string] == 40} {
3094 3094 $sha1entry delete 0 end
3095 3095 }
3096 3096 }
3097 3097
3098 3098 proc sha1change {n1 n2 op} {
3099 3099 global sha1string currentid sha1but
3100 3100 if {$sha1string == {}
3101 3101 || ([info exists currentid] && $sha1string == $currentid)} {
3102 3102 set state disabled
3103 3103 } else {
3104 3104 set state normal
3105 3105 }
3106 3106 if {[$sha1but cget -state] == $state} return
3107 3107 if {$state == "normal"} {
3108 3108 $sha1but conf -state normal -relief raised -text "Goto: "
3109 3109 } else {
3110 3110 $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
3111 3111 }
3112 3112 }
3113 3113
3114 3114 proc gotocommit {} {
3115 3115 global sha1string currentid idline tagids
3116 3116 global lineid numcommits
3117 3117
3118 3118 if {$sha1string == {}
3119 3119 || ([info exists currentid] && $sha1string == $currentid)} return
3120 3120 if {[info exists tagids($sha1string)]} {
3121 3121 set id $tagids($sha1string)
3122 3122 } else {
3123 3123 set id [string tolower $sha1string]
3124 3124 if {[regexp {^[0-9a-f]{4,39}$} $id]} {
3125 3125 set matches {}
3126 3126 for {set l 0} {$l < $numcommits} {incr l} {
3127 3127 if {[string match $id* $lineid($l)]} {
3128 3128 lappend matches $lineid($l)
3129 3129 }
3130 3130 }
3131 3131 if {$matches ne {}} {
3132 3132 if {[llength $matches] > 1} {
3133 3133 error_popup "Short SHA1 id $id is ambiguous"
3134 3134 return
3135 3135 }
3136 3136 set id [lindex $matches 0]
3137 3137 }
3138 3138 }
3139 3139 }
3140 3140 if {[info exists idline($id)]} {
3141 3141 selectline $idline($id) 1
3142 3142 return
3143 3143 }
3144 3144 if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
3145 3145 set type "SHA1 id"
3146 3146 } else {
3147 3147 set type "Tag"
3148 3148 }
3149 3149 error_popup "$type $sha1string is not known"
3150 3150 }
3151 3151
3152 3152 proc lineenter {x y id} {
3153 3153 global hoverx hovery hoverid hovertimer
3154 3154 global commitinfo canv
3155 3155
3156 3156 if {![info exists commitinfo($id)]} return
3157 3157 set hoverx $x
3158 3158 set hovery $y
3159 3159 set hoverid $id
3160 3160 if {[info exists hovertimer]} {
3161 3161 after cancel $hovertimer
3162 3162 }
3163 3163 set hovertimer [after 500 linehover]
3164 3164 $canv delete hover
3165 3165 }
3166 3166
3167 3167 proc linemotion {x y id} {
3168 3168 global hoverx hovery hoverid hovertimer
3169 3169
3170 3170 if {[info exists hoverid] && $id == $hoverid} {
3171 3171 set hoverx $x
3172 3172 set hovery $y
3173 3173 if {[info exists hovertimer]} {
3174 3174 after cancel $hovertimer
3175 3175 }
3176 3176 set hovertimer [after 500 linehover]
3177 3177 }
3178 3178 }
3179 3179
3180 3180 proc lineleave {id} {
3181 3181 global hoverid hovertimer canv
3182 3182
3183 3183 if {[info exists hoverid] && $id == $hoverid} {
3184 3184 $canv delete hover
3185 3185 if {[info exists hovertimer]} {
3186 3186 after cancel $hovertimer
3187 3187 unset hovertimer
3188 3188 }
3189 3189 unset hoverid
3190 3190 }
3191 3191 }
3192 3192
3193 3193 proc linehover {} {
3194 3194 global hoverx hovery hoverid hovertimer
3195 3195 global canv linespc lthickness
3196 3196 global commitinfo mainfont
3197 3197
3198 3198 set text [lindex $commitinfo($hoverid) 0]
3199 3199 set ymax [lindex [$canv cget -scrollregion] 3]
3200 3200 if {$ymax == {}} return
3201 3201 set yfrac [lindex [$canv yview] 0]
3202 3202 set x [expr {$hoverx + 2 * $linespc}]
3203 3203 set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
3204 3204 set x0 [expr {$x - 2 * $lthickness}]
3205 3205 set y0 [expr {$y - 2 * $lthickness}]
3206 3206 set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
3207 3207 set y1 [expr {$y + $linespc + 2 * $lthickness}]
3208 3208 set t [$canv create rectangle $x0 $y0 $x1 $y1 \
3209 3209 -fill \#ffff80 -outline black -width 1 -tags hover]
3210 3210 $canv raise $t
3211 3211 set t [$canv create text $x $y -anchor nw -text $text -tags hover]
3212 3212 $canv raise $t
3213 3213 }
3214 3214
3215 3215 proc clickisonarrow {id y} {
3216 3216 global mainline mainlinearrow sidelines lthickness
3217 3217
3218 3218 set thresh [expr {2 * $lthickness + 6}]
3219 3219 if {[info exists mainline($id)]} {
3220 3220 if {$mainlinearrow($id) ne "none"} {
3221 3221 if {abs([lindex $mainline($id) 1] - $y) < $thresh} {
3222 3222 return "up"
3223 3223 }
3224 3224 }
3225 3225 }
3226 3226 if {[info exists sidelines($id)]} {
3227 3227 foreach ls $sidelines($id) {
3228 3228 set coords [lindex $ls 0]
3229 3229 set arrow [lindex $ls 2]
3230 3230 if {$arrow eq "first" || $arrow eq "both"} {
3231 3231 if {abs([lindex $coords 1] - $y) < $thresh} {
3232 3232 return "up"
3233 3233 }
3234 3234 }
3235 3235 if {$arrow eq "last" || $arrow eq "both"} {
3236 3236 if {abs([lindex $coords end] - $y) < $thresh} {
3237 3237 return "down"
3238 3238 }
3239 3239 }
3240 3240 }
3241 3241 }
3242 3242 return {}
3243 3243 }
3244 3244
3245 3245 proc arrowjump {id dirn y} {
3246 3246 global mainline sidelines canv
3247 3247
3248 3248 set yt {}
3249 3249 if {$dirn eq "down"} {
3250 3250 if {[info exists mainline($id)]} {
3251 3251 set y1 [lindex $mainline($id) 1]
3252 3252 if {$y1 > $y} {
3253 3253 set yt $y1
3254 3254 }
3255 3255 }
3256 3256 if {[info exists sidelines($id)]} {
3257 3257 foreach ls $sidelines($id) {
3258 3258 set y1 [lindex $ls 0 1]
3259 3259 if {$y1 > $y && ($yt eq {} || $y1 < $yt)} {
3260 3260 set yt $y1
3261 3261 }
3262 3262 }
3263 3263 }
3264 3264 } else {
3265 3265 if {[info exists sidelines($id)]} {
3266 3266 foreach ls $sidelines($id) {
3267 3267 set y1 [lindex $ls 0 end]
3268 3268 if {$y1 < $y && ($yt eq {} || $y1 > $yt)} {
3269 3269 set yt $y1
3270 3270 }
3271 3271 }
3272 3272 }
3273 3273 }
3274 3274 if {$yt eq {}} return
3275 3275 set ymax [lindex [$canv cget -scrollregion] 3]
3276 3276 if {$ymax eq {} || $ymax <= 0} return
3277 3277 set view [$canv yview]
3278 3278 set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
3279 3279 set yfrac [expr {$yt / $ymax - $yspan / 2}]
3280 3280 if {$yfrac < 0} {
3281 3281 set yfrac 0
3282 3282 }
3283 3283 $canv yview moveto $yfrac
3284 3284 }
3285 3285
3286 3286 proc lineclick {x y id isnew} {
3287 3287 global ctext commitinfo children cflist canv thickerline
3288 3288
3289 3289 unmarkmatches
3290 3290 unselectline
3291 3291 normalline
3292 3292 $canv delete hover
3293 3293 # draw this line thicker than normal
3294 3294 drawlines $id 1
3295 3295 set thickerline $id
3296 3296 if {$isnew} {
3297 3297 set ymax [lindex [$canv cget -scrollregion] 3]
3298 3298 if {$ymax eq {}} return
3299 3299 set yfrac [lindex [$canv yview] 0]
3300 3300 set y [expr {$y + $yfrac * $ymax}]
3301 3301 }
3302 3302 set dirn [clickisonarrow $id $y]
3303 3303 if {$dirn ne {}} {
3304 3304 arrowjump $id $dirn $y
3305 3305 return
3306 3306 }
3307 3307
3308 3308 if {$isnew} {
3309 3309 addtohistory [list lineclick $x $y $id 0]
3310 3310 }
3311 3311 # fill the details pane with info about this line
3312 3312 $ctext conf -state normal
3313 3313 $ctext delete 0.0 end
3314 3314 $ctext tag conf link -foreground blue -underline 1
3315 3315 $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3316 3316 $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3317 3317 $ctext insert end "Parent:\t"
3318 3318 $ctext insert end $id [list link link0]
3319 3319 $ctext tag bind link0 <1> [list selbyid $id]
3320 3320 set info $commitinfo($id)
3321 3321 $ctext insert end "\n\t[lindex $info 0]\n"
3322 3322 $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
3323 3323 $ctext insert end "\tDate:\t[lindex $info 2]\n"
3324 3324 if {[info exists children($id)]} {
3325 3325 $ctext insert end "\nChildren:"
3326 3326 set i 0
3327 3327 foreach child $children($id) {
3328 3328 incr i
3329 3329 set info $commitinfo($child)
3330 3330 $ctext insert end "\n\t"
3331 3331 $ctext insert end $child [list link link$i]
3332 3332 $ctext tag bind link$i <1> [list selbyid $child]
3333 3333 $ctext insert end "\n\t[lindex $info 0]"
3334 3334 $ctext insert end "\n\tAuthor:\t[lindex $info 1]"
3335 3335 $ctext insert end "\n\tDate:\t[lindex $info 2]\n"
3336 3336 }
3337 3337 }
3338 3338 $ctext conf -state disabled
3339 3339
3340 3340 $cflist delete 0 end
3341 3341 }
3342 3342
3343 3343 proc normalline {} {
3344 3344 global thickerline
3345 3345 if {[info exists thickerline]} {
3346 3346 drawlines $thickerline 0
3347 3347 unset thickerline
3348 3348 }
3349 3349 }
3350 3350
3351 3351 proc selbyid {id} {
3352 3352 global idline
3353 3353 if {[info exists idline($id)]} {
3354 3354 selectline $idline($id) 1
3355 3355 }
3356 3356 }
3357 3357
3358 3358 proc mstime {} {
3359 3359 global startmstime
3360 3360 if {![info exists startmstime]} {
3361 3361 set startmstime [clock clicks -milliseconds]
3362 3362 }
3363 3363 return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
3364 3364 }
3365 3365
3366 3366 proc rowmenu {x y id} {
3367 3367 global rowctxmenu idline selectedline rowmenuid
3368 3368
3369 3369 if {![info exists selectedline] || $idline($id) eq $selectedline} {
3370 3370 set state disabled
3371 3371 } else {
3372 3372 set state normal
3373 3373 }
3374 3374 $rowctxmenu entryconfigure 0 -state $state
3375 3375 $rowctxmenu entryconfigure 1 -state $state
3376 3376 $rowctxmenu entryconfigure 2 -state $state
3377 3377 set rowmenuid $id
3378 3378 tk_popup $rowctxmenu $x $y
3379 3379 }
3380 3380
3381 3381 proc diffvssel {dirn} {
3382 3382 global rowmenuid selectedline lineid
3383 3383
3384 3384 if {![info exists selectedline]} return
3385 3385 if {$dirn} {
3386 3386 set oldid $lineid($selectedline)
3387 3387 set newid $rowmenuid
3388 3388 } else {
3389 3389 set oldid $rowmenuid
3390 3390 set newid $lineid($selectedline)
3391 3391 }
3392 3392 addtohistory [list doseldiff $oldid $newid]
3393 3393 doseldiff $oldid $newid
3394 3394 }
3395 3395
3396 3396 proc doseldiff {oldid newid} {
3397 3397 global ctext cflist
3398 3398 global commitinfo
3399 3399
3400 3400 $ctext conf -state normal
3401 3401 $ctext delete 0.0 end
3402 3402 $ctext mark set fmark.0 0.0
3403 3403 $ctext mark gravity fmark.0 left
3404 3404 $cflist delete 0 end
3405 3405 $cflist insert end "Top"
3406 3406 $ctext insert end "From "
3407 3407 $ctext tag conf link -foreground blue -underline 1
3408 3408 $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3409 3409 $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3410 3410 $ctext tag bind link0 <1> [list selbyid $oldid]
3411 3411 $ctext insert end $oldid [list link link0]
3412 3412 $ctext insert end "\n "
3413 3413 $ctext insert end [lindex $commitinfo($oldid) 0]
3414 3414 $ctext insert end "\n\nTo "
3415 3415 $ctext tag bind link1 <1> [list selbyid $newid]
3416 3416 $ctext insert end $newid [list link link1]
3417 3417 $ctext insert end "\n "
3418 3418 $ctext insert end [lindex $commitinfo($newid) 0]
3419 3419 $ctext insert end "\n"
3420 3420 $ctext conf -state disabled
3421 3421 $ctext tag delete Comments
3422 3422 $ctext tag remove found 1.0 end
3423 3423 startdiff [list $newid $oldid]
3424 3424 }
3425 3425
3426 3426 proc mkpatch {} {
3427 3427 global rowmenuid currentid commitinfo patchtop patchnum
3428 3428
3429 3429 if {![info exists currentid]} return
3430 3430 set oldid $currentid
3431 3431 set oldhead [lindex $commitinfo($oldid) 0]
3432 3432 set newid $rowmenuid
3433 3433 set newhead [lindex $commitinfo($newid) 0]
3434 3434 set top .patch
3435 3435 set patchtop $top
3436 3436 catch {destroy $top}
3437 3437 toplevel $top
3438 3438 label $top.title -text "Generate patch"
3439 3439 grid $top.title - -pady 10
3440 3440 label $top.from -text "From:"
3441 3441 entry $top.fromsha1 -width 40 -relief flat
3442 3442 $top.fromsha1 insert 0 $oldid
3443 3443 $top.fromsha1 conf -state readonly
3444 3444 grid $top.from $top.fromsha1 -sticky w
3445 3445 entry $top.fromhead -width 60 -relief flat
3446 3446 $top.fromhead insert 0 $oldhead
3447 3447 $top.fromhead conf -state readonly
3448 3448 grid x $top.fromhead -sticky w
3449 3449 label $top.to -text "To:"
3450 3450 entry $top.tosha1 -width 40 -relief flat
3451 3451 $top.tosha1 insert 0 $newid
3452 3452 $top.tosha1 conf -state readonly
3453 3453 grid $top.to $top.tosha1 -sticky w
3454 3454 entry $top.tohead -width 60 -relief flat
3455 3455 $top.tohead insert 0 $newhead
3456 3456 $top.tohead conf -state readonly
3457 3457 grid x $top.tohead -sticky w
3458 3458 button $top.rev -text "Reverse" -command mkpatchrev -padx 5
3459 3459 grid $top.rev x -pady 10
3460 3460 label $top.flab -text "Output file:"
3461 3461 entry $top.fname -width 60
3462 3462 $top.fname insert 0 [file normalize "patch$patchnum.patch"]
3463 3463 incr patchnum
3464 3464 grid $top.flab $top.fname -sticky w
3465 3465 frame $top.buts
3466 3466 button $top.buts.gen -text "Generate" -command mkpatchgo
3467 3467 button $top.buts.can -text "Cancel" -command mkpatchcan
3468 3468 grid $top.buts.gen $top.buts.can
3469 3469 grid columnconfigure $top.buts 0 -weight 1 -uniform a
3470 3470 grid columnconfigure $top.buts 1 -weight 1 -uniform a
3471 3471 grid $top.buts - -pady 10 -sticky ew
3472 3472 focus $top.fname
3473 3473 }
3474 3474
3475 3475 proc mkpatchrev {} {
3476 3476 global patchtop
3477 3477
3478 3478 set oldid [$patchtop.fromsha1 get]
3479 3479 set oldhead [$patchtop.fromhead get]
3480 3480 set newid [$patchtop.tosha1 get]
3481 3481 set newhead [$patchtop.tohead get]
3482 3482 foreach e [list fromsha1 fromhead tosha1 tohead] \
3483 3483 v [list $newid $newhead $oldid $oldhead] {
3484 3484 $patchtop.$e conf -state normal
3485 3485 $patchtop.$e delete 0 end
3486 3486 $patchtop.$e insert 0 $v
3487 3487 $patchtop.$e conf -state readonly
3488 3488 }
3489 3489 }
3490 3490
3491 3491 proc mkpatchgo {} {
3492 3492 global patchtop env
3493 3493
3494 3494 set oldid [$patchtop.fromsha1 get]
3495 3495 set newid [$patchtop.tosha1 get]
3496 3496 set fname [$patchtop.fname get]
3497 3497 if {[catch {exec $env(HG) --config ui.report_untrusted=false debug-diff-tree -p $oldid $newid >$fname &} err]} {
3498 3498 error_popup "Error creating patch: $err"
3499 3499 }
3500 3500 catch {destroy $patchtop}
3501 3501 unset patchtop
3502 3502 }
3503 3503
3504 3504 proc mkpatchcan {} {
3505 3505 global patchtop
3506 3506
3507 3507 catch {destroy $patchtop}
3508 3508 unset patchtop
3509 3509 }
3510 3510
3511 3511 proc mktag {} {
3512 3512 global rowmenuid mktagtop commitinfo
3513 3513
3514 3514 set top .maketag
3515 3515 set mktagtop $top
3516 3516 catch {destroy $top}
3517 3517 toplevel $top
3518 3518 label $top.title -text "Create tag"
3519 3519 grid $top.title - -pady 10
3520 3520 label $top.id -text "ID:"
3521 3521 entry $top.sha1 -width 40 -relief flat
3522 3522 $top.sha1 insert 0 $rowmenuid
3523 3523 $top.sha1 conf -state readonly
3524 3524 grid $top.id $top.sha1 -sticky w
3525 3525 entry $top.head -width 60 -relief flat
3526 3526 $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3527 3527 $top.head conf -state readonly
3528 3528 grid x $top.head -sticky w
3529 3529 label $top.tlab -text "Tag name:"
3530 3530 entry $top.tag -width 60
3531 3531 grid $top.tlab $top.tag -sticky w
3532 3532 frame $top.buts
3533 3533 button $top.buts.gen -text "Create" -command mktaggo
3534 3534 button $top.buts.can -text "Cancel" -command mktagcan
3535 3535 grid $top.buts.gen $top.buts.can
3536 3536 grid columnconfigure $top.buts 0 -weight 1 -uniform a
3537 3537 grid columnconfigure $top.buts 1 -weight 1 -uniform a
3538 3538 grid $top.buts - -pady 10 -sticky ew
3539 3539 focus $top.tag
3540 3540 }
3541 3541
3542 3542 proc domktag {} {
3543 3543 global mktagtop env tagids idtags
3544 3544
3545 3545 set id [$mktagtop.sha1 get]
3546 3546 set tag [$mktagtop.tag get]
3547 3547 if {$tag == {}} {
3548 3548 error_popup "No tag name specified"
3549 3549 return
3550 3550 }
3551 3551 if {[info exists tagids($tag)]} {
3552 3552 error_popup "Tag \"$tag\" already exists"
3553 3553 return
3554 3554 }
3555 3555 if {[catch {
3556 3556 set out [exec $env(HG) --config ui.report_untrusted=false tag -r $id $tag]
3557 3557 } err]} {
3558 3558 error_popup "Error creating tag: $err"
3559 3559 return
3560 3560 }
3561 3561
3562 3562 set tagids($tag) $id
3563 3563 lappend idtags($id) $tag
3564 3564 redrawtags $id
3565 3565 }
3566 3566
3567 3567 proc redrawtags {id} {
3568 3568 global canv linehtag idline idpos selectedline
3569 3569
3570 3570 if {![info exists idline($id)]} return
3571 3571 $canv delete tag.$id
3572 3572 set xt [eval drawtags $id $idpos($id)]
3573 3573 $canv coords $linehtag($idline($id)) $xt [lindex $idpos($id) 2]
3574 3574 if {[info exists selectedline] && $selectedline == $idline($id)} {
3575 3575 selectline $selectedline 0
3576 3576 }
3577 3577 }
3578 3578
3579 3579 proc mktagcan {} {
3580 3580 global mktagtop
3581 3581
3582 3582 catch {destroy $mktagtop}
3583 3583 unset mktagtop
3584 3584 }
3585 3585
3586 3586 proc mktaggo {} {
3587 3587 domktag
3588 3588 mktagcan
3589 3589 }
3590 3590
3591 3591 proc writecommit {} {
3592 3592 global rowmenuid wrcomtop commitinfo wrcomcmd
3593 3593
3594 3594 set top .writecommit
3595 3595 set wrcomtop $top
3596 3596 catch {destroy $top}
3597 3597 toplevel $top
3598 3598 label $top.title -text "Write commit to file"
3599 3599 grid $top.title - -pady 10
3600 3600 label $top.id -text "ID:"
3601 3601 entry $top.sha1 -width 40 -relief flat
3602 3602 $top.sha1 insert 0 $rowmenuid
3603 3603 $top.sha1 conf -state readonly
3604 3604 grid $top.id $top.sha1 -sticky w
3605 3605 entry $top.head -width 60 -relief flat
3606 3606 $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3607 3607 $top.head conf -state readonly
3608 3608 grid x $top.head -sticky w
3609 3609 label $top.clab -text "Command:"
3610 3610 entry $top.cmd -width 60 -textvariable wrcomcmd
3611 3611 grid $top.clab $top.cmd -sticky w -pady 10
3612 3612 label $top.flab -text "Output file:"
3613 3613 entry $top.fname -width 60
3614 3614 $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
3615 3615 grid $top.flab $top.fname -sticky w
3616 3616 frame $top.buts
3617 3617 button $top.buts.gen -text "Write" -command wrcomgo
3618 3618 button $top.buts.can -text "Cancel" -command wrcomcan
3619 3619 grid $top.buts.gen $top.buts.can
3620 3620 grid columnconfigure $top.buts 0 -weight 1 -uniform a
3621 3621 grid columnconfigure $top.buts 1 -weight 1 -uniform a
3622 3622 grid $top.buts - -pady 10 -sticky ew
3623 3623 focus $top.fname
3624 3624 }
3625 3625
3626 3626 proc wrcomgo {} {
3627 3627 global wrcomtop
3628 3628
3629 3629 set id [$wrcomtop.sha1 get]
3630 3630 set cmd "echo $id | [$wrcomtop.cmd get]"
3631 3631 set fname [$wrcomtop.fname get]
3632 3632 if {[catch {exec sh -c $cmd > $fname &} err]} {
3633 3633 error_popup "Error writing commit: $err"
3634 3634 }
3635 3635 catch {destroy $wrcomtop}
3636 3636 unset wrcomtop
3637 3637 }
3638 3638
3639 3639 proc wrcomcan {} {
3640 3640 global wrcomtop
3641 3641
3642 3642 catch {destroy $wrcomtop}
3643 3643 unset wrcomtop
3644 3644 }
3645 3645
3646 3646 proc listrefs {id} {
3647 3647 global idtags idheads idotherrefs
3648 3648
3649 3649 set x {}
3650 3650 if {[info exists idtags($id)]} {
3651 3651 set x $idtags($id)
3652 3652 }
3653 3653 set y {}
3654 3654 if {[info exists idheads($id)]} {
3655 3655 set y $idheads($id)
3656 3656 }
3657 3657 set z {}
3658 3658 if {[info exists idotherrefs($id)]} {
3659 3659 set z $idotherrefs($id)
3660 3660 }
3661 3661 return [list $x $y $z]
3662 3662 }
3663 3663
3664 3664 proc rereadrefs {} {
3665 3665 global idtags idheads idotherrefs
3666 3666 global tagids headids otherrefids
3667 3667
3668 3668 set refids [concat [array names idtags] \
3669 3669 [array names idheads] [array names idotherrefs]]
3670 3670 foreach id $refids {
3671 3671 if {![info exists ref($id)]} {
3672 3672 set ref($id) [listrefs $id]
3673 3673 }
3674 3674 }
3675 3675 foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
3676 3676 catch {unset $v}
3677 3677 }
3678 3678 readrefs
3679 3679 set refids [lsort -unique [concat $refids [array names idtags] \
3680 3680 [array names idheads] [array names idotherrefs]]]
3681 3681 foreach id $refids {
3682 3682 set v [listrefs $id]
3683 3683 if {![info exists ref($id)] || $ref($id) != $v} {
3684 3684 redrawtags $id
3685 3685 }
3686 3686 }
3687 3687 }
3688 3688
3689 3689 proc showtag {tag isnew} {
3690 3690 global ctext cflist tagcontents tagids linknum
3691 3691
3692 3692 if {$isnew} {
3693 3693 addtohistory [list showtag $tag 0]
3694 3694 }
3695 3695 $ctext conf -state normal
3696 3696 $ctext delete 0.0 end
3697 3697 set linknum 0
3698 3698 if {[info exists tagcontents($tag)]} {
3699 3699 set text $tagcontents($tag)
3700 3700 } else {
3701 3701 set text "Tag: $tag\nId: $tagids($tag)"
3702 3702 }
3703 3703 appendwithlinks $text
3704 3704 $ctext conf -state disabled
3705 3705 $cflist delete 0 end
3706 3706 }
3707 3707
3708 3708 proc doquit {} {
3709 3709 global stopped
3710 3710 set stopped 100
3711 3711 destroy .
3712 3712 }
3713 3713
3714 3714 # defaults...
3715 3715 set datemode 0
3716 3716 set boldnames 0
3717 3717 set diffopts "-U 5 -p"
3718 3718 set wrcomcmd "\"\$HG\" --config ui.report_untrusted=false debug-diff-tree --stdin -p --pretty"
3719 3719
3720 3720 set mainfont {Helvetica 9}
3721 3721 set textfont {Courier 9}
3722 3722 set findmergefiles 0
3723 3723 set gaudydiff 0
3724 3724 set maxgraphpct 50
3725 3725 set maxwidth 16
3726 3726
3727 3727 set colors {green red blue magenta darkgrey brown orange}
3728 3728
3729 3729 catch {source ~/.gitk}
3730 3730
3731 3731 set namefont $mainfont
3732 3732 if {$boldnames} {
3733 3733 lappend namefont bold
3734 3734 }
3735 3735
3736 3736 set revtreeargs {}
3737 3737 foreach arg $argv {
3738 3738 switch -regexp -- $arg {
3739 3739 "^$" { }
3740 3740 "^-b" { set boldnames 1 }
3741 3741 "^-d" { set datemode 1 }
3742 3742 default {
3743 3743 lappend revtreeargs $arg
3744 3744 }
3745 3745 }
3746 3746 }
3747 3747
3748 3748 set history {}
3749 3749 set historyindex 0
3750 3750
3751 3751 set stopped 0
3752 3752 set redisplaying 0
3753 3753 set stuffsaved 0
3754 3754 set patchnum 0
3755 3755 setcoords
3756 3756 makewindow
3757 3757 readrefs
3758 3758 getcommits $revtreeargs
@@ -1,450 +1,450 b''
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from common import NoRepo, converter_source, converter_sink
9 9 from cvs import convert_cvs
10 10 from git import convert_git
11 11 from hg import mercurial_source, mercurial_sink
12 12 from subversion import convert_svn, debugsvnlog
13 13
14 14 import os, shlex, shutil
15 15 from mercurial import hg, ui, util, commands
16 16 from mercurial.i18n import _
17 17
18 18 commands.norepo += " convert debugsvnlog"
19 19
20 20 converters = [convert_cvs, convert_git, convert_svn, mercurial_source,
21 21 mercurial_sink]
22 22
23 23 def convertsource(ui, path, **opts):
24 24 for c in converters:
25 25 try:
26 26 return c.getcommit and c(ui, path, **opts)
27 27 except (AttributeError, NoRepo):
28 28 pass
29 29 raise util.Abort('%s: unknown repository type' % path)
30 30
31 31 def convertsink(ui, path):
32 32 if not os.path.isdir(path):
33 33 raise util.Abort("%s: not a directory" % path)
34 34 for c in converters:
35 35 try:
36 36 return c.putcommit and c(ui, path)
37 37 except (AttributeError, NoRepo):
38 38 pass
39 39 raise util.Abort('%s: unknown repository type' % path)
40 40
41 41 class convert(object):
42 42 def __init__(self, ui, source, dest, revmapfile, filemapper, opts):
43 43
44 44 self.source = source
45 45 self.dest = dest
46 46 self.ui = ui
47 47 self.opts = opts
48 48 self.commitcache = {}
49 49 self.revmapfile = revmapfile
50 50 self.revmapfilefd = None
51 51 self.authors = {}
52 52 self.authorfile = None
53 53 self.mapfile = filemapper
54 54
55 55 self.map = {}
56 56 try:
57 57 origrevmapfile = open(self.revmapfile, 'r')
58 58 for l in origrevmapfile:
59 59 sv, dv = l[:-1].split()
60 60 self.map[sv] = dv
61 61 origrevmapfile.close()
62 62 except IOError:
63 63 pass
64 64
65 65 # Read first the dst author map if any
66 66 authorfile = self.dest.authorfile()
67 67 if authorfile and os.path.exists(authorfile):
68 68 self.readauthormap(authorfile)
69 69 # Extend/Override with new author map if necessary
70 70 if opts.get('authors'):
71 71 self.readauthormap(opts.get('authors'))
72 72 self.authorfile = self.dest.authorfile()
73 73
74 74 def walktree(self, heads):
75 75 '''Return a mapping that identifies the uncommitted parents of every
76 76 uncommitted changeset.'''
77 77 visit = heads
78 78 known = {}
79 79 parents = {}
80 80 while visit:
81 81 n = visit.pop(0)
82 82 if n in known or n in self.map: continue
83 83 known[n] = 1
84 84 self.commitcache[n] = self.source.getcommit(n)
85 85 cp = self.commitcache[n].parents
86 86 parents[n] = []
87 87 for p in cp:
88 88 parents[n].append(p)
89 89 visit.append(p)
90 90
91 91 return parents
92 92
93 93 def toposort(self, parents):
94 94 '''Return an ordering such that every uncommitted changeset is
95 95 preceeded by all its uncommitted ancestors.'''
96 96 visit = parents.keys()
97 97 seen = {}
98 98 children = {}
99 99
100 100 while visit:
101 101 n = visit.pop(0)
102 102 if n in seen: continue
103 103 seen[n] = 1
104 104 # Ensure that nodes without parents are present in the 'children'
105 105 # mapping.
106 106 children.setdefault(n, [])
107 107 for p in parents[n]:
108 108 if not p in self.map:
109 109 visit.append(p)
110 110 children.setdefault(p, []).append(n)
111 111
112 112 s = []
113 113 removed = {}
114 114 visit = children.keys()
115 115 while visit:
116 116 n = visit.pop(0)
117 117 if n in removed: continue
118 118 dep = 0
119 119 if n in parents:
120 120 for p in parents[n]:
121 121 if p in self.map: continue
122 122 if p not in removed:
123 123 # we're still dependent
124 124 visit.append(n)
125 125 dep = 1
126 126 break
127 127
128 128 if not dep:
129 129 # all n's parents are in the list
130 130 removed[n] = 1
131 131 if n not in self.map:
132 132 s.append(n)
133 133 if n in children:
134 134 for c in children[n]:
135 135 visit.insert(0, c)
136 136
137 137 if self.opts.get('datesort'):
138 138 depth = {}
139 139 for n in s:
140 140 depth[n] = 0
141 141 pl = [p for p in self.commitcache[n].parents
142 142 if p not in self.map]
143 143 if pl:
144 144 depth[n] = max([depth[p] for p in pl]) + 1
145 145
146 146 s = [(depth[n], self.commitcache[n].date, n) for n in s]
147 147 s.sort()
148 148 s = [e[2] for e in s]
149 149
150 150 return s
151 151
152 152 def mapentry(self, src, dst):
153 153 if self.revmapfilefd is None:
154 154 try:
155 155 self.revmapfilefd = open(self.revmapfile, "a")
156 156 except IOError, (errno, strerror):
157 157 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
158 158 self.map[src] = dst
159 159 self.revmapfilefd.write("%s %s\n" % (src, dst))
160 160 self.revmapfilefd.flush()
161 161
162 162 def writeauthormap(self):
163 163 authorfile = self.authorfile
164 164 if authorfile:
165 165 self.ui.status('Writing author map file %s\n' % authorfile)
166 166 ofile = open(authorfile, 'w+')
167 167 for author in self.authors:
168 168 ofile.write("%s=%s\n" % (author, self.authors[author]))
169 169 ofile.close()
170 170
171 171 def readauthormap(self, authorfile):
172 172 afile = open(authorfile, 'r')
173 173 for line in afile:
174 174 try:
175 175 srcauthor = line.split('=')[0].strip()
176 176 dstauthor = line.split('=')[1].strip()
177 177 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
178 178 self.ui.status(
179 179 'Overriding mapping for author %s, was %s, will be %s\n'
180 180 % (srcauthor, self.authors[srcauthor], dstauthor))
181 181 else:
182 182 self.ui.debug('Mapping author %s to %s\n'
183 183 % (srcauthor, dstauthor))
184 184 self.authors[srcauthor] = dstauthor
185 185 except IndexError:
186 186 self.ui.warn(
187 187 'Ignoring bad line in author file map %s: %s\n'
188 188 % (authorfile, line))
189 189 afile.close()
190 190
191 191 def copy(self, rev):
192 192 commit = self.commitcache[rev]
193 193 do_copies = hasattr(self.dest, 'copyfile')
194 194 filenames = []
195 195
196 196 files, copies = self.source.getchanges(rev)
197 197 for f, v in files:
198 198 newf = self.mapfile(f)
199 199 if not newf:
200 200 continue
201 201 filenames.append(newf)
202 202 try:
203 203 data = self.source.getfile(f, v)
204 204 except IOError, inst:
205 205 self.dest.delfile(newf)
206 206 else:
207 207 e = self.source.getmode(f, v)
208 208 self.dest.putfile(newf, e, data)
209 209 if do_copies:
210 210 if f in copies:
211 211 copyf = self.mapfile(copies[f])
212 212 if copyf:
213 213 # Merely marks that a copy happened.
214 214 self.dest.copyfile(copyf, newf)
215 215
216 216 parents = [self.map[r] for r in commit.parents]
217 217 newnode = self.dest.putcommit(filenames, parents, commit)
218 218 self.mapentry(rev, newnode)
219 219
220 220 def convert(self):
221 221 try:
222 222 self.dest.before()
223 223 self.source.setrevmap(self.map)
224 224 self.ui.status("scanning source...\n")
225 225 heads = self.source.getheads()
226 226 parents = self.walktree(heads)
227 227 self.ui.status("sorting...\n")
228 228 t = self.toposort(parents)
229 229 num = len(t)
230 230 c = None
231 231
232 232 self.ui.status("converting...\n")
233 233 for c in t:
234 234 num -= 1
235 235 desc = self.commitcache[c].desc
236 236 if "\n" in desc:
237 237 desc = desc.splitlines()[0]
238 238 author = self.commitcache[c].author
239 239 author = self.authors.get(author, author)
240 240 self.commitcache[c].author = author
241 241 self.ui.status("%d %s\n" % (num, desc))
242 242 self.copy(c)
243 243
244 244 tags = self.source.gettags()
245 245 ctags = {}
246 246 for k in tags:
247 247 v = tags[k]
248 248 if v in self.map:
249 249 ctags[k] = self.map[v]
250 250
251 251 if c and ctags:
252 252 nrev = self.dest.puttags(ctags)
253 253 # write another hash correspondence to override the previous
254 254 # one so we don't end up with extra tag heads
255 255 if nrev:
256 256 self.mapentry(c, nrev)
257 257
258 258 self.writeauthormap()
259 259 finally:
260 260 self.cleanup()
261 261
262 262 def cleanup(self):
263 263 self.dest.after()
264 264 if self.revmapfilefd:
265 265 self.revmapfilefd.close()
266 266
267 267 def rpairs(name):
268 268 e = len(name)
269 269 while e != -1:
270 270 yield name[:e], name[e+1:]
271 271 e = name.rfind('/', 0, e)
272 272
273 273 class filemapper(object):
274 274 '''Map and filter filenames when importing.
275 275 A name can be mapped to itself, a new name, or None (omit from new
276 276 repository).'''
277 277
278 278 def __init__(self, ui, path=None):
279 279 self.ui = ui
280 280 self.include = {}
281 281 self.exclude = {}
282 282 self.rename = {}
283 283 if path:
284 284 if self.parse(path):
285 285 raise util.Abort(_('errors in filemap'))
286 286
287 287 def parse(self, path):
288 288 errs = 0
289 289 def check(name, mapping, listname):
290 290 if name in mapping:
291 291 self.ui.warn(_('%s:%d: %r already in %s list\n') %
292 292 (lex.infile, lex.lineno, name, listname))
293 293 return 1
294 294 return 0
295 295 lex = shlex.shlex(open(path), path, True)
296 296 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
297 297 cmd = lex.get_token()
298 298 while cmd:
299 299 if cmd == 'include':
300 300 name = lex.get_token()
301 301 errs += check(name, self.exclude, 'exclude')
302 302 self.include[name] = name
303 303 elif cmd == 'exclude':
304 304 name = lex.get_token()
305 305 errs += check(name, self.include, 'include')
306 306 errs += check(name, self.rename, 'rename')
307 307 self.exclude[name] = name
308 308 elif cmd == 'rename':
309 309 src = lex.get_token()
310 310 dest = lex.get_token()
311 311 errs += check(src, self.exclude, 'exclude')
312 312 self.rename[src] = dest
313 313 elif cmd == 'source':
314 314 errs += self.parse(lex.get_token())
315 315 else:
316 316 self.ui.warn(_('%s:%d: unknown directive %r\n') %
317 317 (lex.infile, lex.lineno, cmd))
318 318 errs += 1
319 319 cmd = lex.get_token()
320 320 return errs
321 321
322 322 def lookup(self, name, mapping):
323 323 for pre, suf in rpairs(name):
324 324 try:
325 325 return mapping[pre], pre, suf
326 326 except KeyError, err:
327 327 pass
328 328 return '', name, ''
329
329
330 330 def __call__(self, name):
331 331 if self.include:
332 332 inc = self.lookup(name, self.include)[0]
333 333 else:
334 334 inc = name
335 335 if self.exclude:
336 336 exc = self.lookup(name, self.exclude)[0]
337 337 else:
338 338 exc = ''
339 339 if not inc or exc:
340 340 return None
341 341 newpre, pre, suf = self.lookup(name, self.rename)
342 342 if newpre:
343 343 if newpre == '.':
344 344 return suf
345 345 if suf:
346 346 return newpre + '/' + suf
347 347 return newpre
348 348 return name
349 349
350 350 def _convert(ui, src, dest=None, revmapfile=None, **opts):
351 351 """Convert a foreign SCM repository to a Mercurial one.
352 352
353 353 Accepted source formats:
354 354 - GIT
355 355 - CVS
356 356 - SVN
357 357
358 358 Accepted destination formats:
359 359 - Mercurial
360 360
361 361 If no revision is given, all revisions will be converted. Otherwise,
362 362 convert will only import up to the named revision (given in a format
363 363 understood by the source).
364 364
365 365 If no destination directory name is specified, it defaults to the
366 366 basename of the source with '-hg' appended. If the destination
367 367 repository doesn't exist, it will be created.
368 368
369 369 If <revmapfile> isn't given, it will be put in a default location
370 370 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
371 371 file that maps each source commit ID to the destination ID for
372 372 that revision, like so:
373 373 <source ID> <destination ID>
374 374
375 375 If the file doesn't exist, it's automatically created. It's updated
376 376 on each commit copied, so convert-repo can be interrupted and can
377 377 be run repeatedly to copy new commits.
378 378
379 379 The [username mapping] file is a simple text file that maps each source
380 380 commit author to a destination commit author. It is handy for source SCMs
381 381 that use unix logins to identify authors (eg: CVS). One line per author
382 382 mapping and the line format is:
383 383 srcauthor=whatever string you want
384 384 """
385 385
386 386 util._encoding = 'UTF-8'
387 387
388 388 if not dest:
389 389 dest = hg.defaultdest(src) + "-hg"
390 390 ui.status("assuming destination %s\n" % dest)
391 391
392 392 # Try to be smart and initalize things when required
393 393 created = False
394 394 if os.path.isdir(dest):
395 395 if len(os.listdir(dest)) > 0:
396 396 try:
397 397 hg.repository(ui, dest)
398 398 ui.status("destination %s is a Mercurial repository\n" % dest)
399 399 except hg.RepoError:
400 400 raise util.Abort(
401 401 "destination directory %s is not empty.\n"
402 402 "Please specify an empty directory to be initialized\n"
403 403 "or an already initialized mercurial repository"
404 404 % dest)
405 405 else:
406 406 ui.status("initializing destination %s repository\n" % dest)
407 407 hg.repository(ui, dest, create=True)
408 408 created = True
409 409 elif os.path.exists(dest):
410 410 raise util.Abort("destination %s exists and is not a directory" % dest)
411 411 else:
412 412 ui.status("initializing destination %s repository\n" % dest)
413 413 hg.repository(ui, dest, create=True)
414 414 created = True
415 415
416 416 destc = convertsink(ui, dest)
417 417
418 418 try:
419 419 srcc = convertsource(ui, src, rev=opts.get('rev'))
420 420 except Exception:
421 421 if created:
422 422 shutil.rmtree(dest, True)
423 423 raise
424 424
425 425 if not revmapfile:
426 426 try:
427 427 revmapfile = destc.revmapfile()
428 428 except:
429 429 revmapfile = os.path.join(destc, "map")
430 430
431 431
432 432 c = convert(ui, srcc, destc, revmapfile, filemapper(ui, opts['filemap']),
433 433 opts)
434 434 c.convert()
435 435
436 436
437 437 cmdtable = {
438 438 "convert":
439 439 (_convert,
440 440 [('A', 'authors', '', 'username mapping filename'),
441 441 ('', 'filemap', '', 'remap file names using contents of file'),
442 442 ('r', 'rev', '', 'import up to target revision REV'),
443 443 ('', 'datesort', None, 'try to sort changesets by date')],
444 444 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
445 445 "debugsvnlog":
446 446 (debugsvnlog,
447 447 [],
448 448 'hg debugsvnlog'),
449 449 }
450 450
@@ -1,136 +1,136 b''
1 1 # common code for the convert extension
2 2 import base64
3 3 import cPickle as pickle
4 4
5 5 def encodeargs(args):
6 6 def encodearg(s):
7 7 lines = base64.encodestring(s)
8 8 lines = [l.splitlines()[0] for l in lines]
9 9 return ''.join(lines)
10
10
11 11 s = pickle.dumps(args)
12 12 return encodearg(s)
13 13
14 14 def decodeargs(s):
15 15 s = base64.decodestring(s)
16 16 return pickle.loads(s)
17 17
18 18 class NoRepo(Exception): pass
19 19
20 20 class commit(object):
21 21 def __init__(self, author, date, desc, parents, branch=None, rev=None):
22 22 self.author = author
23 23 self.date = date
24 24 self.desc = desc
25 25 self.parents = parents
26 26 self.branch = branch
27 27 self.rev = rev
28 28
29 29 class converter_source(object):
30 30 """Conversion source interface"""
31 31
32 32 def __init__(self, ui, path, rev=None):
33 33 """Initialize conversion source (or raise NoRepo("message")
34 34 exception if path is not a valid repository)"""
35 35 self.ui = ui
36 36 self.path = path
37 37 self.rev = rev
38 38
39 39 self.encoding = 'utf-8'
40 40
41 41 def setrevmap(self, revmap):
42 42 """set the map of already-converted revisions"""
43 43 pass
44 44
45 45 def getheads(self):
46 46 """Return a list of this repository's heads"""
47 47 raise NotImplementedError()
48 48
49 49 def getfile(self, name, rev):
50 50 """Return file contents as a string"""
51 51 raise NotImplementedError()
52 52
53 53 def getmode(self, name, rev):
54 54 """Return file mode, eg. '', 'x', or 'l'"""
55 55 raise NotImplementedError()
56 56
57 57 def getchanges(self, version):
58 58 """Returns a tuple of (files, copies)
59 59 Files is a sorted list of (filename, id) tuples for all files changed
60 60 in version, where id is the source revision id of the file.
61 61
62 62 copies is a dictionary of dest: source
63 63 """
64 64 raise NotImplementedError()
65 65
66 66 def getcommit(self, version):
67 67 """Return the commit object for version"""
68 68 raise NotImplementedError()
69 69
70 70 def gettags(self):
71 71 """Return the tags as a dictionary of name: revision"""
72 72 raise NotImplementedError()
73 73
74 74 def recode(self, s, encoding=None):
75 75 if not encoding:
76 76 encoding = self.encoding or 'utf-8'
77 77
78 78 try:
79 79 return s.decode(encoding).encode("utf-8")
80 80 except:
81 81 try:
82 82 return s.decode("latin-1").encode("utf-8")
83 83 except:
84 84 return s.decode(encoding, "replace").encode("utf-8")
85 85
86 86 class converter_sink(object):
87 87 """Conversion sink (target) interface"""
88 88
89 89 def __init__(self, ui, path):
90 90 """Initialize conversion sink (or raise NoRepo("message")
91 91 exception if path is not a valid repository)"""
92 92 raise NotImplementedError()
93 93
94 94 def getheads(self):
95 95 """Return a list of this repository's heads"""
96 96 raise NotImplementedError()
97 97
98 98 def revmapfile(self):
99 99 """Path to a file that will contain lines
100 100 source_rev_id sink_rev_id
101 101 mapping equivalent revision identifiers for each system."""
102 102 raise NotImplementedError()
103 103
104 104 def authorfile(self):
105 105 """Path to a file that will contain lines
106 106 srcauthor=dstauthor
107 107 mapping equivalent authors identifiers for each system."""
108 108 return None
109 109
110 110 def putfile(self, f, e, data):
111 111 """Put file for next putcommit().
112 112 f: path to file
113 113 e: '', 'x', or 'l' (regular file, executable, or symlink)
114 114 data: file contents"""
115 115 raise NotImplementedError()
116 116
117 117 def delfile(self, f):
118 118 """Delete file for next putcommit().
119 119 f: path to file"""
120 120 raise NotImplementedError()
121 121
122 122 def putcommit(self, files, parents, commit):
123 123 """Create a revision with all changed files listed in 'files'
124 124 and having listed parents. 'commit' is a commit object containing
125 125 at a minimum the author, date, and message for this changeset.
126 126 Called after putfile() and delfile() calls. Note that the sink
127 127 repository is not told to update itself to a particular revision
128 128 (or even what that revision would be) before it receives the
129 129 file data."""
130 130 raise NotImplementedError()
131 131
132 132 def puttags(self, tags):
133 133 """Put tags into sink.
134 134 tags: {tagname: sink_rev_id, ...}"""
135 135 raise NotImplementedError()
136 136
@@ -1,178 +1,178 b''
1 1 # hg backend for convert extension
2 2
3 3 # Note for hg->hg conversion: Old versions of Mercurial didn't trim
4 4 # the whitespace from the ends of commit messages, but new versions
5 5 # do. Changesets created by those older versions, then converted, may
6 6 # thus have different hashes for changesets that are otherwise
7 7 # identical.
8 8
9 9
10 10 import os, time
11 11 from mercurial.i18n import _
12 12 from mercurial.node import *
13 13 from mercurial import hg, lock, revlog, util
14 14
15 15 from common import NoRepo, commit, converter_source, converter_sink
16 16
17 17 class mercurial_sink(converter_sink):
18 18 def __init__(self, ui, path):
19 19 self.path = path
20 20 self.ui = ui
21 21 try:
22 22 self.repo = hg.repository(self.ui, path)
23 23 except:
24 24 raise NoRepo("could not open hg repo %s as sink" % path)
25 25 self.lock = None
26 26 self.wlock = None
27 27 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
28 28
29 29 def before(self):
30 30 self.wlock = self.repo.wlock()
31 31 self.lock = self.repo.lock()
32 32
33 33 def after(self):
34 34 self.lock = None
35 35 self.wlock = None
36 36
37 37 def revmapfile(self):
38 38 return os.path.join(self.path, ".hg", "shamap")
39 39
40 40 def authorfile(self):
41 41 return os.path.join(self.path, ".hg", "authormap")
42 42
43 43 def getheads(self):
44 44 h = self.repo.changelog.heads()
45 45 return [ hex(x) for x in h ]
46 46
47 47 def putfile(self, f, e, data):
48 48 self.repo.wwrite(f, data, e)
49 49 if f not in self.repo.dirstate:
50 50 self.repo.dirstate.add(f)
51 51
52 52 def copyfile(self, source, dest):
53 53 self.repo.copy(source, dest)
54 54
55 55 def delfile(self, f):
56 56 try:
57 57 os.unlink(self.repo.wjoin(f))
58 58 #self.repo.remove([f])
59 59 except:
60 60 pass
61 61
62 62 def putcommit(self, files, parents, commit):
63 63 if not files:
64 64 return hex(self.repo.changelog.tip())
65 65
66 66 seen = {hex(nullid): 1}
67 67 pl = []
68 68 for p in parents:
69 69 if p not in seen:
70 70 pl.append(p)
71 71 seen[p] = 1
72 72 parents = pl
73 73
74 74 if len(parents) < 2: parents.append("0" * 40)
75 75 if len(parents) < 2: parents.append("0" * 40)
76 76 p2 = parents.pop(0)
77 77
78 78 text = commit.desc
79 79 extra = {}
80 80 if self.branchnames and commit.branch:
81 81 extra['branch'] = commit.branch
82 82 if commit.rev:
83 83 extra['convert_revision'] = commit.rev
84 84
85 85 while parents:
86 86 p1 = p2
87 87 p2 = parents.pop(0)
88 88 a = self.repo.rawcommit(files, text, commit.author, commit.date,
89 89 bin(p1), bin(p2), extra=extra)
90 90 self.repo.dirstate.invalidate()
91 91 text = "(octopus merge fixup)\n"
92 92 p2 = hg.hex(self.repo.changelog.tip())
93 93
94 94 return p2
95 95
96 96 def puttags(self, tags):
97 97 try:
98 98 old = self.repo.wfile(".hgtags").read()
99 99 oldlines = old.splitlines(1)
100 100 oldlines.sort()
101 101 except:
102 102 oldlines = []
103 103
104 104 k = tags.keys()
105 105 k.sort()
106 106 newlines = []
107 107 for tag in k:
108 108 newlines.append("%s %s\n" % (tags[tag], tag))
109 109
110 110 newlines.sort()
111 111
112 112 if newlines != oldlines:
113 113 self.ui.status("updating tags\n")
114 114 f = self.repo.wfile(".hgtags", "w")
115 115 f.write("".join(newlines))
116 116 f.close()
117 117 if not oldlines: self.repo.add([".hgtags"])
118 118 date = "%s 0" % int(time.mktime(time.gmtime()))
119 119 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
120 120 date, self.repo.changelog.tip(), nullid)
121 121 return hex(self.repo.changelog.tip())
122 122
123 123 class mercurial_source(converter_source):
124 124 def __init__(self, ui, path, rev=None):
125 125 converter_source.__init__(self, ui, path, rev)
126 126 self.repo = hg.repository(self.ui, path)
127 127 self.lastrev = None
128 128 self.lastctx = None
129 129
130 130 def changectx(self, rev):
131 131 if self.lastrev != rev:
132 132 self.lastctx = self.repo.changectx(rev)
133 133 self.lastrev = rev
134 134 return self.lastctx
135 135
136 136 def getheads(self):
137 137 if self.rev:
138 138 return [hex(self.repo.changectx(self.rev).node())]
139 139 else:
140 140 return [hex(node) for node in self.repo.heads()]
141 141
142 142 def getfile(self, name, rev):
143 143 try:
144 144 return self.changectx(rev).filectx(name).data()
145 145 except revlog.LookupError, err:
146 146 raise IOError(err)
147 147
148 148 def getmode(self, name, rev):
149 149 m = self.changectx(rev).manifest()
150 150 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
151 151
152 152 def getchanges(self, rev):
153 153 ctx = self.changectx(rev)
154 154 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
155 155 changes = [(name, rev) for name in m + a + r]
156 156 changes.sort()
157 157 return (changes, self.getcopies(ctx))
158 158
159 159 def getcopies(self, ctx):
160 160 added = self.repo.status(ctx.parents()[0].node(), ctx.node())[1]
161 161 copies = {}
162 162 for name in added:
163 163 try:
164 164 copies[name] = ctx.filectx(name).renamed()[0]
165 165 except TypeError:
166 166 pass
167 167 return copies
168
168
169 169 def getcommit(self, rev):
170 170 ctx = self.changectx(rev)
171 171 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
172 172 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
173 173 desc=ctx.description(), parents=parents,
174 174 branch=ctx.branch())
175 175
176 176 def gettags(self):
177 177 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
178 178 return dict([(name, hex(node)) for name, node in tags])
@@ -1,645 +1,645 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4 #
5 5 # Configuration options:
6 6 #
7 7 # convert.svn.trunk
8 8 # Relative path to the trunk (default: "trunk")
9 9 # convert.svn.branches
10 10 # Relative path to tree of branches (default: "branches")
11 11 #
12 12 # Set these in a hgrc, or on the command line as follows:
13 13 #
14 14 # hg convert --config convert.svn.trunk=wackoname [...]
15 15
16 16 import locale
17 17 import os
18 18 import sys
19 19 import cPickle as pickle
20 20 from mercurial import util
21 21
22 22 # Subversion stuff. Works best with very recent Python SVN bindings
23 23 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
24 24 # these bindings.
25 25
26 26 from cStringIO import StringIO
27 27
28 28 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
29 29
30 30 try:
31 31 from svn.core import SubversionException, Pool
32 32 import svn
33 33 import svn.client
34 34 import svn.core
35 35 import svn.ra
36 36 import svn.delta
37 37 import transport
38 38 except ImportError:
39 39 pass
40 40
41 41 def geturl(path):
42 42 try:
43 43 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
44 44 except SubversionException:
45 45 pass
46 46 if os.path.isdir(path):
47 47 return 'file://%s' % os.path.normpath(os.path.abspath(path))
48 48 return path
49 49
50 50 def optrev(number):
51 51 optrev = svn.core.svn_opt_revision_t()
52 52 optrev.kind = svn.core.svn_opt_revision_number
53 53 optrev.value.number = number
54 54 return optrev
55 55
56 56 class changedpath(object):
57 57 def __init__(self, p):
58 58 self.copyfrom_path = p.copyfrom_path
59 59 self.copyfrom_rev = p.copyfrom_rev
60 60 self.action = p.action
61 61
62 62 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
63 63 strict_node_history=False):
64 64 protocol = -1
65 65 def receiver(orig_paths, revnum, author, date, message, pool):
66 66 if orig_paths is not None:
67 67 for k, v in orig_paths.iteritems():
68 68 orig_paths[k] = changedpath(v)
69 69 pickle.dump((orig_paths, revnum, author, date, message),
70 fp, protocol)
71
70 fp, protocol)
71
72 72 try:
73 73 # Use an ra of our own so that our parent can consume
74 74 # our results without confusing the server.
75 75 t = transport.SvnRaTransport(url=url)
76 76 svn.ra.get_log(t.ra, paths, start, end, limit,
77 77 discover_changed_paths,
78 78 strict_node_history,
79 79 receiver)
80 80 except SubversionException, (inst, num):
81 81 pickle.dump(num, fp, protocol)
82 82 else:
83 83 pickle.dump(None, fp, protocol)
84 84 fp.close()
85 85
86 86 def debugsvnlog(ui, **opts):
87 87 """Fetch SVN log in a subprocess and channel them back to parent to
88 88 avoid memory collection issues.
89 89 """
90 90 util.set_binary(sys.stdin)
91 91 util.set_binary(sys.stdout)
92 92 args = decodeargs(sys.stdin.read())
93 93 get_log_child(sys.stdout, *args)
94 94
95 95 # SVN conversion code stolen from bzr-svn and tailor
96 96 class convert_svn(converter_source):
97 97 def __init__(self, ui, url, rev=None):
98 98 super(convert_svn, self).__init__(ui, url, rev=rev)
99 99
100 100 try:
101 101 SubversionException
102 102 except NameError:
103 103 msg = 'subversion python bindings could not be loaded\n'
104 104 ui.warn(msg)
105 105 raise NoRepo(msg)
106 106
107 107 self.encoding = locale.getpreferredencoding()
108 108 self.lastrevs = {}
109 109
110 110 latest = None
111 111 if rev:
112 112 try:
113 113 latest = int(rev)
114 114 except ValueError:
115 115 raise NoRepo('svn: revision %s is not an integer' % rev)
116 116 try:
117 117 # Support file://path@rev syntax. Useful e.g. to convert
118 118 # deleted branches.
119 119 at = url.rfind('@')
120 120 if at >= 0:
121 121 latest = int(url[at+1:])
122 122 url = url[:at]
123 123 except ValueError, e:
124 124 pass
125 125 self.url = geturl(url)
126 126 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
127 127 try:
128 128 self.transport = transport.SvnRaTransport(url=self.url)
129 129 self.ra = self.transport.ra
130 130 self.ctx = self.transport.client
131 131 self.base = svn.ra.get_repos_root(self.ra)
132 132 self.module = self.url[len(self.base):]
133 133 self.modulemap = {} # revision, module
134 134 self.commits = {}
135 135 self.paths = {}
136 136 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
137 137 except SubversionException, e:
138 138 raise NoRepo("couldn't open SVN repo %s" % self.url)
139 139
140 140 try:
141 141 self.get_blacklist()
142 142 except IOError, e:
143 143 pass
144 144
145 145 self.last_changed = self.latest(self.module, latest)
146 146
147 147 self.head = self.revid(self.last_changed)
148 148
149 149 def setrevmap(self, revmap):
150 150 lastrevs = {}
151 151 for revid in revmap.keys():
152 152 uuid, module, revnum = self.revsplit(revid)
153 153 lastrevnum = lastrevs.setdefault(module, revnum)
154 154 if revnum > lastrevnum:
155 155 lastrevs[module] = revnum
156 156 self.lastrevs = lastrevs
157 157
158 158 def exists(self, path, optrev):
159 159 try:
160 160 return svn.client.ls(self.url.rstrip('/') + '/' + path,
161 161 optrev, False, self.ctx)
162 162 except SubversionException, err:
163 163 return []
164 164
165 165 def getheads(self):
166 166 # detect standard /branches, /tags, /trunk layout
167 167 rev = optrev(self.last_changed)
168 168 rpath = self.url.strip('/')
169 169 cfgtrunk = self.ui.config('convert', 'svn.trunk')
170 170 cfgbranches = self.ui.config('convert', 'svn.branches')
171 171 trunk = (cfgtrunk or 'trunk').strip('/')
172 172 branches = (cfgbranches or 'branches').strip('/')
173 173 if self.exists(trunk, rev) and self.exists(branches, rev):
174 174 self.ui.note('found trunk at %r and branches at %r\n' %
175 175 (trunk, branches))
176 176 oldmodule = self.module
177 177 self.module += '/' + trunk
178 178 lt = self.latest(self.module, self.last_changed)
179 179 self.head = self.revid(lt)
180 180 self.heads = [self.head]
181 181 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
182 182 self.ctx)
183 183 for branch in branchnames.keys():
184 184 if oldmodule:
185 185 module = '/' + oldmodule + '/' + branches + '/' + branch
186 186 else:
187 187 module = '/' + branches + '/' + branch
188 188 brevnum = self.latest(module, self.last_changed)
189 189 brev = self.revid(brevnum, module)
190 190 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
191 191 self.heads.append(brev)
192 192 elif cfgtrunk or cfgbranches:
193 193 raise util.Abort('trunk/branch layout expected, but not found')
194 194 else:
195 195 self.ui.note('working with one branch\n')
196 196 self.heads = [self.head]
197 197 return self.heads
198 198
199 199 def getfile(self, file, rev):
200 200 data, mode = self._getfile(file, rev)
201 201 self.modecache[(file, rev)] = mode
202 202 return data
203 203
204 204 def getmode(self, file, rev):
205 205 return self.modecache[(file, rev)]
206 206
207 207 def getchanges(self, rev):
208 208 self.modecache = {}
209 209 (paths, parents) = self.paths[rev]
210 210 files, copies = self.expandpaths(rev, paths, parents)
211 211 files.sort()
212 212 files = zip(files, [rev] * len(files))
213 213
214 214 # caller caches the result, so free it here to release memory
215 215 del self.paths[rev]
216 216 return (files, copies)
217 217
218 218 def getcommit(self, rev):
219 219 if rev not in self.commits:
220 220 uuid, module, revnum = self.revsplit(rev)
221 221 self.module = module
222 222 self.reparent(module)
223 223 stop = self.lastrevs.get(module, 0)
224 224 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
225 225 commit = self.commits[rev]
226 226 # caller caches the result, so free it here to release memory
227 227 del self.commits[rev]
228 228 return commit
229 229
230 230 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
231 231 strict_node_history=False):
232 232
233 233 def parent(fp):
234 234 while True:
235 235 entry = pickle.load(fp)
236 236 try:
237 237 orig_paths, revnum, author, date, message = entry
238 238 except:
239 239 if entry is None:
240 240 break
241 241 raise SubversionException("child raised exception", entry)
242 242 yield entry
243
243
244 244 args = [self.url, paths, start, end, limit, discover_changed_paths,
245 245 strict_node_history]
246 246 arg = encodeargs(args)
247 247 hgexe = util.hgexecutable()
248 248 cmd = '"%s "debugsvnlog""' % util.shellquote(hgexe)
249 249 stdin, stdout = os.popen2(cmd, 'b')
250
250
251 251 stdin.write(arg)
252 252 stdin.close()
253 253
254 254 for p in parent(stdout):
255 255 yield p
256 256
257 257 def gettags(self):
258 258 tags = {}
259 259 start = self.revnum(self.head)
260 260 try:
261 261 for entry in self.get_log(['/tags'], 0, start):
262 262 orig_paths, revnum, author, date, message = entry
263 263 for path in orig_paths:
264 264 if not path.startswith('/tags/'):
265 265 continue
266 266 ent = orig_paths[path]
267 267 source = ent.copyfrom_path
268 268 rev = ent.copyfrom_rev
269 269 tag = path.split('/', 2)[2]
270 270 tags[tag] = self.revid(rev, module=source)
271 271 except SubversionException, (inst, num):
272 272 self.ui.note('no tags found at revision %d\n' % start)
273 273 return tags
274 274
275 275 # -- helper functions --
276 276
277 277 def revid(self, revnum, module=None):
278 278 if not module:
279 279 module = self.module
280 280 return (u"svn:%s%s@%s" % (self.uuid, module, revnum)).decode(self.encoding)
281 281
282 282 def revnum(self, rev):
283 283 return int(rev.split('@')[-1])
284 284
285 285 def revsplit(self, rev):
286 286 url, revnum = rev.encode(self.encoding).split('@', 1)
287 287 revnum = int(revnum)
288 288 parts = url.split('/', 1)
289 289 uuid = parts.pop(0)[4:]
290 290 mod = ''
291 291 if parts:
292 292 mod = '/' + parts[0]
293 293 return uuid, mod, revnum
294 294
295 295 def latest(self, path, stop=0):
296 296 'find the latest revision affecting path, up to stop'
297 297 if not stop:
298 298 stop = svn.ra.get_latest_revnum(self.ra)
299 299 try:
300 300 self.reparent('')
301 301 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
302 302 self.reparent(self.module)
303 303 except SubversionException:
304 304 dirent = None
305 305 if not dirent:
306 306 raise util.Abort('%s not found up to revision %d' % (path, stop))
307 307
308 308 return dirent.created_rev
309 309
310 310 def get_blacklist(self):
311 311 """Avoid certain revision numbers.
312 312 It is not uncommon for two nearby revisions to cancel each other
313 313 out, e.g. 'I copied trunk into a subdirectory of itself instead
314 314 of making a branch'. The converted repository is significantly
315 315 smaller if we ignore such revisions."""
316 316 self.blacklist = set()
317 317 blacklist = self.blacklist
318 318 for line in file("blacklist.txt", "r"):
319 319 if not line.startswith("#"):
320 320 try:
321 321 svn_rev = int(line.strip())
322 322 blacklist.add(svn_rev)
323 323 except ValueError, e:
324 324 pass # not an integer or a comment
325 325
326 326 def is_blacklisted(self, svn_rev):
327 327 return svn_rev in self.blacklist
328 328
329 329 def reparent(self, module):
330 330 svn_url = self.base + module
331 331 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
332 332 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
333 333
334 334 def expandpaths(self, rev, paths, parents):
335 335 def get_entry_from_path(path, module=self.module):
336 336 # Given the repository url of this wc, say
337 337 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
338 338 # extract the "entry" portion (a relative path) from what
339 339 # svn log --xml says, ie
340 340 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
341 341 # that is to say "tests/PloneTestCase.py"
342 342 if path.startswith(module):
343 343 relative = path[len(module):]
344 344 if relative.startswith('/'):
345 345 return relative[1:]
346 346 else:
347 347 return relative
348 348
349 349 # The path is outside our tracked tree...
350 350 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
351 351 return None
352 352
353 353 entries = []
354 354 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
355 355 copies = {}
356 356 revnum = self.revnum(rev)
357 357
358 358 if revnum in self.modulemap:
359 359 new_module = self.modulemap[revnum]
360 360 if new_module != self.module:
361 361 self.module = new_module
362 362 self.reparent(self.module)
363 363
364 364 for path, ent in paths:
365 365 entrypath = get_entry_from_path(path, module=self.module)
366 366 entry = entrypath.decode(self.encoding)
367 367
368 368 kind = svn.ra.check_path(self.ra, entrypath, revnum)
369 369 if kind == svn.core.svn_node_file:
370 370 if ent.copyfrom_path:
371 371 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
372 372 if copyfrom_path:
373 373 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
374 374 # It's probably important for hg that the source
375 375 # exists in the revision's parent, not just the
376 376 # ent.copyfrom_rev
377 377 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
378 378 if fromkind != 0:
379 379 copies[self.recode(entry)] = self.recode(copyfrom_path)
380 380 entries.append(self.recode(entry))
381 381 elif kind == 0: # gone, but had better be a deleted *file*
382 382 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
383 383
384 384 # if a branch is created but entries are removed in the same
385 385 # changeset, get the right fromrev
386 386 if parents:
387 387 uuid, old_module, fromrev = self.revsplit(parents[0])
388 388 else:
389 389 fromrev = revnum - 1
390 390 # might always need to be revnum - 1 in these 3 lines?
391 391 old_module = self.modulemap.get(fromrev, self.module)
392 392
393 393 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
394 394 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
395 395
396 396 def lookup_parts(p):
397 397 rc = None
398 398 parts = p.split("/")
399 399 for i in range(len(parts)):
400 400 part = "/".join(parts[:i])
401 401 info = part, copyfrom.get(part, None)
402 402 if info[1] is not None:
403 403 self.ui.debug("Found parent directory %s\n" % info[1])
404 404 rc = info
405 405 return rc
406 406
407 407 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
408 408
409 409 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
410 410
411 411 # need to remove fragment from lookup_parts and replace with copyfrom_path
412 412 if frompath is not None:
413 413 self.ui.debug("munge-o-matic\n")
414 414 self.ui.debug(entrypath + '\n')
415 415 self.ui.debug(entrypath[len(frompath):] + '\n')
416 416 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
417 417 fromrev = froment.copyfrom_rev
418 418 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
419 419
420 420 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
421 421 if fromkind == svn.core.svn_node_file: # a deleted file
422 422 entries.append(self.recode(entry))
423 423 elif fromkind == svn.core.svn_node_dir:
424 424 # print "Deleted/moved non-file:", revnum, path, ent
425 425 # children = self._find_children(path, revnum - 1)
426 426 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
427 427 # Sometimes this is tricky. For example: in
428 428 # The Subversion Repository revision 6940 a dir
429 429 # was copied and one of its files was deleted
430 430 # from the new location in the same commit. This
431 431 # code can't deal with that yet.
432 432 if ent.action == 'C':
433 433 children = self._find_children(path, fromrev)
434 434 else:
435 435 oroot = entrypath.strip('/')
436 436 nroot = path.strip('/')
437 437 children = self._find_children(oroot, fromrev)
438 438 children = [s.replace(oroot,nroot) for s in children]
439 439 # Mark all [files, not directories] as deleted.
440 440 for child in children:
441 441 # Can we move a child directory and its
442 442 # parent in the same commit? (probably can). Could
443 443 # cause problems if instead of revnum -1,
444 444 # we have to look in (copyfrom_path, revnum - 1)
445 445 entrypath = get_entry_from_path("/" + child, module=old_module)
446 446 if entrypath:
447 447 entry = self.recode(entrypath.decode(self.encoding))
448 448 if entry in copies:
449 449 # deleted file within a copy
450 450 del copies[entry]
451 451 else:
452 452 entries.append(entry)
453 453 else:
454 454 self.ui.debug('unknown path in revision %d: %s\n' % \
455 455 (revnum, path))
456 456 elif kind == svn.core.svn_node_dir:
457 457 # Should probably synthesize normal file entries
458 458 # and handle as above to clean up copy/rename handling.
459 459
460 460 # If the directory just had a prop change,
461 461 # then we shouldn't need to look for its children.
462 462 # Also this could create duplicate entries. Not sure
463 463 # whether this will matter. Maybe should make entries a set.
464 464 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
465 465 # This will fail if a directory was copied
466 466 # from another branch and then some of its files
467 467 # were deleted in the same transaction.
468 468 children = self._find_children(path, revnum)
469 469 children.sort()
470 470 for child in children:
471 471 # Can we move a child directory and its
472 472 # parent in the same commit? (probably can). Could
473 473 # cause problems if instead of revnum -1,
474 474 # we have to look in (copyfrom_path, revnum - 1)
475 475 entrypath = get_entry_from_path("/" + child, module=self.module)
476 476 # print child, self.module, entrypath
477 477 if entrypath:
478 478 # Need to filter out directories here...
479 479 kind = svn.ra.check_path(self.ra, entrypath, revnum)
480 480 if kind != svn.core.svn_node_dir:
481 481 entries.append(self.recode(entrypath))
482 482
483 483 # Copies here (must copy all from source)
484 484 # Probably not a real problem for us if
485 485 # source does not exist
486 486
487 487 # Can do this with the copy command "hg copy"
488 488 # if ent.copyfrom_path:
489 489 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
490 490 # module=self.module)
491 491 # copyto_entry = entrypath
492 492 #
493 493 # print "copy directory", copyfrom_entry, 'to', copyto_entry
494 494 #
495 495 # copies.append((copyfrom_entry, copyto_entry))
496 496
497 497 if ent.copyfrom_path:
498 498 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
499 499 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
500 500 if copyfrom_entry:
501 501 copyfrom[path] = ent
502 502 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
503 503
504 504 # Good, /probably/ a regular copy. Really should check
505 505 # to see whether the parent revision actually contains
506 506 # the directory in question.
507 507 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
508 508 children.sort()
509 509 for child in children:
510 510 entrypath = get_entry_from_path("/" + child, module=self.module)
511 511 if entrypath:
512 512 entry = entrypath.decode(self.encoding)
513 513 # print "COPY COPY From", copyfrom_entry, entry
514 514 copyto_path = path + entry[len(copyfrom_entry):]
515 515 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
516 516 # print "COPY", entry, "COPY To", copyto_entry
517 517 copies[self.recode(copyto_entry)] = self.recode(entry)
518 518 # copy from quux splort/quuxfile
519 519
520 520 return (entries, copies)
521 521
522 522 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
523 523 self.child_cset = None
524 524 def parselogentry(orig_paths, revnum, author, date, message):
525 525 self.ui.debug("parsing revision %d (%d changes)\n" %
526 526 (revnum, len(orig_paths)))
527 527
528 528 if revnum in self.modulemap:
529 529 new_module = self.modulemap[revnum]
530 530 if new_module != self.module:
531 531 self.module = new_module
532 532 self.reparent(self.module)
533 533
534 534 rev = self.revid(revnum)
535 535 # branch log might return entries for a parent we already have
536 536 if (rev in self.commits or
537 537 (revnum < self.lastrevs.get(self.module, 0))):
538 538 return
539 539
540 540 parents = []
541 541 orig_paths = orig_paths.items()
542 542 orig_paths.sort()
543
543
544 544 # check whether this revision is the start of a branch
545 545 path, ent = orig_paths and orig_paths[0] or (None, None)
546 546 if ent and path == self.module:
547 547 if ent.copyfrom_path:
548 548 # ent.copyfrom_rev may not be the actual last revision
549 549 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
550 550 self.modulemap[prev] = ent.copyfrom_path
551 551 parents = [self.revid(prev, ent.copyfrom_path)]
552 552 self.ui.note('found parent of branch %s at %d: %s\n' % \
553 553 (self.module, prev, ent.copyfrom_path))
554 554 else:
555 555 self.ui.debug("No copyfrom path, don't know what to do.\n")
556 556
557 557 self.modulemap[revnum] = self.module # track backwards in time
558 558
559 559 paths = []
560 560 # filter out unrelated paths
561 561 for path, ent in orig_paths:
562 562 if not path.startswith(self.module):
563 563 self.ui.debug("boring@%s: %s\n" % (revnum, path))
564 564 continue
565 565 paths.append((path, ent))
566 566
567 567 self.paths[rev] = (paths, parents)
568 568
569 569 # Example SVN datetime. Includes microseconds.
570 570 # ISO-8601 conformant
571 571 # '2007-01-04T17:35:00.902377Z'
572 572 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
573 573
574 574 log = message and self.recode(message)
575 575 author = author and self.recode(author) or ''
576 576 try:
577 577 branch = self.module.split("/")[-1]
578 578 if branch == 'trunk':
579 579 branch = ''
580 580 except IndexError:
581 581 branch = None
582 582
583 583 cset = commit(author=author,
584 584 date=util.datestr(date),
585 585 desc=log,
586 586 parents=parents,
587 587 branch=branch,
588 588 rev=rev.encode('utf-8'))
589 589
590 590 self.commits[rev] = cset
591 591 if self.child_cset and not self.child_cset.parents:
592 592 self.child_cset.parents = [rev]
593 593 self.child_cset = cset
594 594
595 595 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
596 596 (self.module, from_revnum, to_revnum))
597 597
598 598 try:
599 599 for entry in self.get_log([self.module], from_revnum, to_revnum):
600 600 orig_paths, revnum, author, date, message = entry
601 601 if self.is_blacklisted(revnum):
602 602 self.ui.note('skipping blacklisted revision %d\n' % revnum)
603 603 continue
604 604 if orig_paths is None:
605 605 self.ui.debug('revision %d has no entries\n' % revnum)
606 606 continue
607 607 parselogentry(orig_paths, revnum, author, date, message)
608 608 except SubversionException, (inst, num):
609 609 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
610 610 raise NoSuchRevision(branch=self,
611 611 revision="Revision number %d" % to_revnum)
612 612 raise
613 613
614 614 def _getfile(self, file, rev):
615 615 io = StringIO()
616 616 # TODO: ra.get_file transmits the whole file instead of diffs.
617 617 mode = ''
618 618 try:
619 619 revnum = self.revnum(rev)
620 620 if self.module != self.modulemap[revnum]:
621 621 self.module = self.modulemap[revnum]
622 622 self.reparent(self.module)
623 623 info = svn.ra.get_file(self.ra, file, revnum, io)
624 624 if isinstance(info, list):
625 625 info = info[-1]
626 626 mode = ("svn:executable" in info) and 'x' or ''
627 627 mode = ("svn:special" in info) and 'l' or mode
628 628 except SubversionException, e:
629 629 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
630 630 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
631 631 if e.apr_err in notfound: # File not found
632 632 raise IOError()
633 633 raise
634 634 data = io.getvalue()
635 635 if mode == 'l':
636 636 link_prefix = "link "
637 637 if data.startswith(link_prefix):
638 638 data = data[len(link_prefix):]
639 639 return data, mode
640 640
641 641 def _find_children(self, path, revnum):
642 642 path = path.strip('/')
643 643 pool = Pool()
644 644 rpath = '/'.join([self.base, path]).strip('/')
645 645 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
@@ -1,215 +1,215 b''
1 1 # extdiff.py - external diff program support for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # The `extdiff' Mercurial extension allows you to use external programs
9 9 # to compare revisions, or revision with working dir. The external diff
10 10 # programs are called with a configurable set of options and two
11 11 # non-option arguments: paths to directories containing snapshots of
12 12 # files to compare.
13 13 #
14 14 # To enable this extension:
15 15 #
16 16 # [extensions]
17 17 # hgext.extdiff =
18 18 #
19 19 # The `extdiff' extension also allows to configure new diff commands, so
20 20 # you do not need to type "hg extdiff -p kdiff3" always.
21 21 #
22 22 # [extdiff]
23 23 # # add new command that runs GNU diff(1) in 'context diff' mode
24 24 # cmd.cdiff = gdiff
25 25 # opts.cdiff = -Nprc5
26 26
27 27 # # add new command called vdiff, runs kdiff3
28 28 # cmd.vdiff = kdiff3
29 29
30 30 # # add new command called meld, runs meld (no need to name twice)
31 31 # cmd.meld =
32 32
33 33 # # add new command called vimdiff, runs gvimdiff with DirDiff plugin
34 34 # #(see http://www.vim.org/scripts/script.php?script_id=102)
35 35 # # Non english user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
36 36 # # your .vimrc
37 37 # cmd.vimdiff = gvim
38 38 # opts.vimdiff = -f '+next' '+execute "DirDiff" argv(0) argv(1)'
39 39 #
40 40 # Each custom diff commands can have two parts: a `cmd' and an `opts'
41 41 # part. The cmd.xxx option defines the name of an executable program
42 42 # that will be run, and opts.xxx defines a set of command-line options
43 43 # which will be inserted to the command between the program name and
44 44 # the files/directories to diff (i.e. the cdiff example above).
45 45 #
46 46 # You can use -I/-X and list of file or directory names like normal
47 47 # "hg diff" command. The `extdiff' extension makes snapshots of only
48 48 # needed files, so running the external diff program will actually be
49 49 # pretty fast (at least faster than having to compare the entire tree).
50 50
51 51 from mercurial.i18n import _
52 52 from mercurial.node import *
53 53 from mercurial import cmdutil, util
54 54 import os, shutil, tempfile
55 55
56 56
57 57 def snapshot_node(ui, repo, files, node, tmproot):
58 58 '''snapshot files as of some revision'''
59 59 mf = repo.changectx(node).manifest()
60 60 dirname = os.path.basename(repo.root)
61 61 if dirname == "":
62 62 dirname = "root"
63 63 dirname = '%s.%s' % (dirname, short(node))
64 64 base = os.path.join(tmproot, dirname)
65 65 os.mkdir(base)
66 66 ui.note(_('making snapshot of %d files from rev %s\n') %
67 (len(files), short(node)))
67 (len(files), short(node)))
68 68 for fn in files:
69 69 if not fn in mf:
70 70 # skipping new file after a merge ?
71 71 continue
72 72 wfn = util.pconvert(fn)
73 73 ui.note(' %s\n' % wfn)
74 74 dest = os.path.join(base, wfn)
75 75 destdir = os.path.dirname(dest)
76 76 if not os.path.isdir(destdir):
77 77 os.makedirs(destdir)
78 78 data = repo.wwritedata(wfn, repo.file(wfn).read(mf[wfn]))
79 79 open(dest, 'wb').write(data)
80 80 return dirname
81 81
82 82
83 83 def snapshot_wdir(ui, repo, files, tmproot):
84 84 '''snapshot files from working directory.
85 85 if not using snapshot, -I/-X does not work and recursive diff
86 86 in tools like kdiff3 and meld displays too many files.'''
87 87 dirname = os.path.basename(repo.root)
88 88 if dirname == "":
89 89 dirname = "root"
90 90 base = os.path.join(tmproot, dirname)
91 91 os.mkdir(base)
92 92 ui.note(_('making snapshot of %d files from working dir\n') %
93 (len(files)))
93 (len(files)))
94 94 for fn in files:
95 95 wfn = util.pconvert(fn)
96 96 ui.note(' %s\n' % wfn)
97 97 dest = os.path.join(base, wfn)
98 98 destdir = os.path.dirname(dest)
99 99 if not os.path.isdir(destdir):
100 100 os.makedirs(destdir)
101 101 fp = open(dest, 'wb')
102 102 for chunk in util.filechunkiter(repo.wopener(wfn)):
103 103 fp.write(chunk)
104 104 return dirname
105
105
106 106
107 107 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
108 108 node1, node2 = cmdutil.revpair(repo, opts['rev'])
109 109 files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts)
110 110 modified, added, removed, deleted, unknown = repo.status(
111 111 node1, node2, files, match=matchfn)[:5]
112 112 if not (modified or added or removed):
113 113 return 0
114 114
115 115 tmproot = tempfile.mkdtemp(prefix='extdiff.')
116 116 dir2root = ''
117 117 try:
118 118 # Always make a copy of node1
119 119 dir1 = snapshot_node(ui, repo, modified + removed, node1, tmproot)
120 120 changes = len(modified) + len(removed) + len(added)
121 121
122 122 # If node2 in not the wc or there is >1 change, copy it
123 123 if node2:
124 124 dir2 = snapshot_node(ui, repo, modified + added, node2, tmproot)
125 125 elif changes > 1:
126 126 dir2 = snapshot_wdir(ui, repo, modified + added, tmproot)
127 127 else:
128 128 # This lets the diff tool open the changed file directly
129 129 dir2 = ''
130 130 dir2root = repo.root
131 131
132 132 # If only one change, diff the files instead of the directories
133 133 if changes == 1 :
134 134 if len(modified):
135 135 dir1 = os.path.join(dir1, util.localpath(modified[0]))
136 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0]))
136 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0]))
137 137 elif len(removed) :
138 138 dir1 = os.path.join(dir1, util.localpath(removed[0]))
139 139 dir2 = os.devnull
140 140 else:
141 141 dir1 = os.devnull
142 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0]))
143
142 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0]))
143
144 144 cmdline = ('%s %s %s %s' %
145 145 (util.shellquote(diffcmd), ' '.join(diffopts),
146 146 util.shellquote(dir1), util.shellquote(dir2)))
147 147 ui.debug('running %r in %s\n' % (cmdline, tmproot))
148 148 util.system(cmdline, cwd=tmproot)
149 149 return 1
150 150 finally:
151 151 ui.note(_('cleaning up temp directory\n'))
152 152 shutil.rmtree(tmproot)
153 153
154 154 def extdiff(ui, repo, *pats, **opts):
155 155 '''use external program to diff repository (or selected files)
156 156
157 157 Show differences between revisions for the specified files, using
158 158 an external program. The default program used is diff, with
159 159 default options "-Npru".
160 160
161 161 To select a different program, use the -p option. The program
162 162 will be passed the names of two directories to compare. To pass
163 163 additional options to the program, use the -o option. These will
164 164 be passed before the names of the directories to compare.
165 165
166 166 When two revision arguments are given, then changes are
167 167 shown between those revisions. If only one revision is
168 168 specified then that revision is compared to the working
169 169 directory, and, when no revisions are specified, the
170 170 working directory files are compared to its parent.'''
171 171 program = opts['program'] or 'diff'
172 172 if opts['program']:
173 173 option = opts['option']
174 174 else:
175 175 option = opts['option'] or ['-Npru']
176 176 return dodiff(ui, repo, program, option, pats, opts)
177 177
178 178 cmdtable = {
179 179 "extdiff":
180 180 (extdiff,
181 181 [('p', 'program', '', _('comparison program to run')),
182 182 ('o', 'option', [], _('pass option to comparison program')),
183 183 ('r', 'rev', [], _('revision')),
184 184 ('I', 'include', [], _('include names matching the given patterns')),
185 185 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
186 186 _('hg extdiff [OPT]... [FILE]...')),
187 187 }
188 188
189 189 def uisetup(ui):
190 190 for cmd, path in ui.configitems('extdiff'):
191 191 if not cmd.startswith('cmd.'): continue
192 192 cmd = cmd[4:]
193 193 if not path: path = cmd
194 194 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
195 195 diffopts = diffopts and [diffopts] or []
196 196 def save(cmd, path, diffopts):
197 197 '''use closure to save diff command to use'''
198 198 def mydiff(ui, repo, *pats, **opts):
199 199 return dodiff(ui, repo, path, diffopts, pats, opts)
200 200 mydiff.__doc__ = '''use %(path)r to diff repository (or selected files)
201 201
202 202 Show differences between revisions for the specified
203 203 files, using the %(path)r program.
204 204
205 205 When two revision arguments are given, then changes are
206 206 shown between those revisions. If only one revision is
207 207 specified then that revision is compared to the working
208 208 directory, and, when no revisions are specified, the
209 209 working directory files are compared to its parent.''' % {
210 210 'path': path,
211 211 }
212 212 return mydiff
213 213 cmdtable[cmd] = (save(cmd, path, diffopts),
214 214 cmdtable['extdiff'][1][1:],
215 215 _('hg %s [OPTION]... [FILE]...') % cmd)
@@ -1,361 +1,361 b''
1 1 # Copyright (C) 2007 Brendan Cully <brendan@kublai.com>
2 2 # Published under the GNU GPL
3 3
4 4 '''
5 5 imerge - interactive merge
6 6 '''
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial.node import *
10 10 from mercurial import commands, cmdutil, fancyopts, hg, merge, util
11 11 import os, tarfile
12 12
13 13 class InvalidStateFileException(Exception): pass
14 14
15 15 class ImergeStateFile(object):
16 16 def __init__(self, im):
17 17 self.im = im
18 18
19 19 def save(self, dest):
20 20 tf = tarfile.open(dest, 'w:gz')
21 21
22 22 st = os.path.join(self.im.path, 'status')
23 23 tf.add(st, os.path.join('.hg', 'imerge', 'status'))
24 24
25 25 for f in self.im.resolved:
26 26 (fd, fo) = self.im.conflicts[f]
27 27 abssrc = self.im.repo.wjoin(fd)
28 28 tf.add(abssrc, fd)
29 29
30 30 tf.close()
31 31
32 32 def load(self, source):
33 33 wlock = self.im.repo.wlock()
34 34 lock = self.im.repo.lock()
35 35
36 36 tf = tarfile.open(source, 'r')
37 37 contents = tf.getnames()
38 38 statusfile = os.path.join('.hg', 'imerge', 'status')
39 39 if statusfile not in contents:
40 40 raise InvalidStateFileException('no status file')
41 41
42 42 tf.extract(statusfile, self.im.repo.root)
43 43 p1, p2 = self.im.load()
44 44 if self.im.repo.dirstate.parents()[0] != p1.node():
45 45 hg.clean(self.im.repo, p1.node())
46 46 self.im.start(p2.node())
47 47 for tarinfo in tf:
48 48 tf.extract(tarinfo, self.im.repo.root)
49 49 self.im.load()
50 50
51 51 class Imerge(object):
52 52 def __init__(self, ui, repo):
53 53 self.ui = ui
54 54 self.repo = repo
55 55
56 56 self.path = repo.join('imerge')
57 57 self.opener = util.opener(self.path)
58 58
59 59 self.wctx = self.repo.workingctx()
60 60 self.conflicts = {}
61 61 self.resolved = []
62 62
63 63 def merging(self):
64 64 return len(self.wctx.parents()) > 1
65 65
66 66 def load(self):
67 67 # status format. \0-delimited file, fields are
68 68 # p1, p2, conflict count, conflict filenames, resolved filenames
69 69 # conflict filenames are tuples of localname, remoteorig, remotenew
70 70
71 71 statusfile = self.opener('status')
72 72
73 73 status = statusfile.read().split('\0')
74 74 if len(status) < 3:
75 75 raise util.Abort('invalid imerge status file')
76 76
77 77 try:
78 78 parents = [self.repo.changectx(n) for n in status[:2]]
79 79 except LookupError:
80 80 raise util.Abort('merge parent %s not in repository' % short(p))
81 81
82 82 status = status[2:]
83 83 conflicts = int(status.pop(0)) * 3
84 84 self.resolved = status[conflicts:]
85 85 for i in xrange(0, conflicts, 3):
86 86 self.conflicts[status[i]] = (status[i+1], status[i+2])
87 87
88 88 return parents
89 89
90 90 def save(self):
91 91 lock = self.repo.lock()
92 92
93 93 if not os.path.isdir(self.path):
94 94 os.mkdir(self.path)
95 95 statusfile = self.opener('status', 'wb')
96 96
97 97 out = [hex(n.node()) for n in self.wctx.parents()]
98 98 out.append(str(len(self.conflicts)))
99 99 conflicts = self.conflicts.items()
100 100 conflicts.sort()
101 101 for fw, fd_fo in conflicts:
102 102 out.append(fw)
103 103 out.extend(fd_fo)
104 104 out.extend(self.resolved)
105 105
106 106 statusfile.write('\0'.join(out))
107 107
108 108 def remaining(self):
109 109 return [f for f in self.conflicts if f not in self.resolved]
110 110
111 111 def filemerge(self, fn):
112 112 wlock = self.repo.wlock()
113 113
114 114 (fd, fo) = self.conflicts[fn]
115 115 p2 = self.wctx.parents()[1]
116 116 return merge.filemerge(self.repo, fn, fd, fo, self.wctx, p2)
117 117
118 118 def start(self, rev=None):
119 119 _filemerge = merge.filemerge
120 120 def filemerge(repo, fw, fd, fo, wctx, mctx):
121 121 self.conflicts[fw] = (fd, fo)
122 122
123 123 merge.filemerge = filemerge
124 124 commands.merge(self.ui, self.repo, rev=rev)
125 125 merge.filemerge = _filemerge
126 126
127 127 self.wctx = self.repo.workingctx()
128 128 self.save()
129 129
130 130 def resume(self):
131 131 self.load()
132 132
133 133 dp = self.repo.dirstate.parents()
134 134 p1, p2 = self.wctx.parents()
135 135 if p1.node() != dp[0] or p2.node() != dp[1]:
136 136 raise util.Abort('imerge state does not match working directory')
137 137
138 138 def next(self):
139 139 remaining = self.remaining()
140 140 return remaining and remaining[0]
141 141
142 142 def resolve(self, files):
143 143 resolved = dict.fromkeys(self.resolved)
144 144 for fn in files:
145 145 if fn not in self.conflicts:
146 146 raise util.Abort('%s is not in the merge set' % fn)
147 147 resolved[fn] = True
148 148 self.resolved = resolved.keys()
149 149 self.resolved.sort()
150 150 self.save()
151 151 return 0
152 152
153 153 def unresolve(self, files):
154 154 resolved = dict.fromkeys(self.resolved)
155 155 for fn in files:
156 156 if fn not in resolved:
157 157 raise util.Abort('%s is not resolved' % fn)
158 158 del resolved[fn]
159 159 self.resolved = resolved.keys()
160 160 self.resolved.sort()
161 161 self.save()
162 162 return 0
163 163
164 164 def pickle(self, dest):
165 165 '''write current merge state to file to be resumed elsewhere'''
166 166 state = ImergeStateFile(self)
167 167 return state.save(dest)
168 168
169 169 def unpickle(self, source):
170 170 '''read merge state from file'''
171 171 state = ImergeStateFile(self)
172 172 return state.load(source)
173 173
174 174 def load(im, source):
175 175 if im.merging():
176 176 raise util.Abort('there is already a merge in progress '
177 177 '(update -C <rev> to abort it)' )
178 178 m, a, r, d = im.repo.status()[:4]
179 179 if m or a or r or d:
180 180 raise util.Abort('working directory has uncommitted changes')
181 181
182 182 rc = im.unpickle(source)
183 183 if not rc:
184 184 status(im)
185 185 return rc
186 186
187 187 def merge_(im, filename=None):
188 188 if not filename:
189 189 filename = im.next()
190 190 if not filename:
191 191 im.ui.write('all conflicts resolved\n')
192 192 return 0
193 193
194 194 rc = im.filemerge(filename)
195 195 if not rc:
196 196 im.resolve([filename])
197 197 if not im.next():
198 198 im.ui.write('all conflicts resolved\n')
199 199 return 0
200 200 return rc
201 201
202 202 def next(im):
203 203 n = im.next()
204 204 if n:
205 205 im.ui.write('%s\n' % n)
206 206 else:
207 207 im.ui.write('all conflicts resolved\n')
208 208 return 0
209 209
210 210 def resolve(im, *files):
211 211 if not files:
212 212 raise util.Abort('resolve requires at least one filename')
213 213 return im.resolve(files)
214 214
215 215 def save(im, dest):
216 216 return im.pickle(dest)
217 217
218 218 def status(im, **opts):
219 219 if not opts.get('resolved') and not opts.get('unresolved'):
220 220 opts['resolved'] = True
221 221 opts['unresolved'] = True
222 222
223 223 if im.ui.verbose:
224 224 p1, p2 = [short(p.node()) for p in im.wctx.parents()]
225 225 im.ui.note(_('merging %s and %s\n') % (p1, p2))
226 226
227 227 conflicts = im.conflicts.keys()
228 228 conflicts.sort()
229 229 remaining = dict.fromkeys(im.remaining())
230 230 st = []
231 231 for fn in conflicts:
232 232 if opts.get('no_status'):
233 233 mode = ''
234 234 elif fn in remaining:
235 235 mode = 'U '
236 236 else:
237 237 mode = 'R '
238 238 if ((opts.get('resolved') and fn not in remaining)
239 239 or (opts.get('unresolved') and fn in remaining)):
240 240 st.append((mode, fn))
241 241 st.sort()
242 242 for (mode, fn) in st:
243 243 if im.ui.verbose:
244 244 fo, fd = im.conflicts[fn]
245 245 if fd != fn:
246 246 fn = '%s (%s)' % (fn, fd)
247 247 im.ui.write('%s%s\n' % (mode, fn))
248 248 if opts.get('unresolved') and not remaining:
249 249 im.ui.write(_('all conflicts resolved\n'))
250 250
251 251 return 0
252 252
253 253 def unresolve(im, *files):
254 254 if not files:
255 255 raise util.Abort('unresolve requires at least one filename')
256 256 return im.unresolve(files)
257 257
258 258 subcmdtable = {
259 259 'load': (load, []),
260 260 'merge': (merge_, []),
261 261 'next': (next, []),
262 262 'resolve': (resolve, []),
263 263 'save': (save, []),
264 264 'status': (status,
265 265 [('n', 'no-status', None, _('hide status prefix')),
266 266 ('', 'resolved', None, _('only show resolved conflicts')),
267 267 ('', 'unresolved', None, _('only show unresolved conflicts'))]),
268 268 'unresolve': (unresolve, [])
269 269 }
270 270
271 271 def dispatch(im, args, opts):
272 272 def complete(s, choices):
273 273 candidates = []
274 274 for choice in choices:
275 275 if choice.startswith(s):
276 276 candidates.append(choice)
277 277 return candidates
278 278
279 279 c, args = args[0], list(args[1:])
280 280 cmd = complete(c, subcmdtable.keys())
281 281 if not cmd:
282 282 raise cmdutil.UnknownCommand('imerge ' + c)
283 283 if len(cmd) > 1:
284 284 cmd.sort()
285 285 raise cmdutil.AmbiguousCommand('imerge ' + c, cmd)
286 286 cmd = cmd[0]
287 287
288 288 func, optlist = subcmdtable[cmd]
289 289 opts = {}
290 290 try:
291 291 args = fancyopts.fancyopts(args, optlist, opts)
292 292 return func(im, *args, **opts)
293 293 except fancyopts.getopt.GetoptError, inst:
294 294 raise cmdutil.ParseError('imerge', '%s: %s' % (cmd, inst))
295 295 except TypeError:
296 296 raise cmdutil.ParseError('imerge', _('%s: invalid arguments') % cmd)
297 297
298 298 def imerge(ui, repo, *args, **opts):
299 299 '''interactive merge
300 300
301 301 imerge lets you split a merge into pieces. When you start a merge
302 302 with imerge, the names of all files with conflicts are recorded.
303 303 You can then merge any of these files, and if the merge is
304 304 successful, they will be marked as resolved. When all files are
305 305 resolved, the merge is complete.
306 306
307 307 If no merge is in progress, hg imerge [rev] will merge the working
308 308 directory with rev (defaulting to the other head if the repository
309 309 only has two heads). You may also resume a saved merge with
310 310 hg imerge load <file>.
311 311
312 312 If a merge is in progress, hg imerge will default to merging the
313 313 next unresolved file.
314 314
315 315 The following subcommands are available:
316 316
317 317 status:
318 318 show the current state of the merge
319 319 next:
320 320 show the next unresolved file merge
321 321 merge [<file>]:
322 322 merge <file>. If the file merge is successful, the file will be
323 323 recorded as resolved. If no file is given, the next unresolved
324 324 file will be merged.
325 325 resolve <file>...:
326 326 mark files as successfully merged
327 327 unresolve <file>...:
328 328 mark files as requiring merging.
329 329 save <file>:
330 330 save the state of the merge to a file to be resumed elsewhere
331 331 load <file>:
332 332 load the state of the merge from a file created by save
333 333 '''
334 334
335 335 im = Imerge(ui, repo)
336 336
337 337 if im.merging():
338 338 im.resume()
339 339 else:
340 340 rev = opts.get('rev')
341 341 if rev and args:
342 342 raise util.Abort('please specify just one revision')
343
343
344 344 if len(args) == 2 and args[0] == 'load':
345 345 pass
346 346 else:
347 347 if args:
348 348 rev = args[0]
349 349 im.start(rev=rev)
350 350 args = ['status']
351 351
352 352 if not args:
353 353 args = ['merge']
354 354
355 355 return dispatch(im, args, opts)
356 356
357 357 cmdtable = {
358 358 '^imerge':
359 359 (imerge,
360 360 [('r', 'rev', '', _('revision to merge'))], 'hg imerge [command]')
361 361 }
@@ -1,382 +1,382 b''
1 1 # record.py
2 2 #
3 3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of
6 6 # the GNU General Public License, incorporated herein by reference.
7 7
8 8 '''interactive change selection during commit'''
9 9
10 10 from mercurial.i18n import _
11 11 from mercurial import cmdutil, commands, cmdutil, hg, mdiff, patch, revlog
12 12 from mercurial import util
13 13 import copy, cStringIO, errno, operator, os, re, shutil, tempfile
14 14
15 15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16 16
17 17 def scanpatch(fp):
18 18 lr = patch.linereader(fp)
19 19
20 20 def scanwhile(first, p):
21 21 lines = [first]
22 22 while True:
23 23 line = lr.readline()
24 24 if not line:
25 25 break
26 26 if p(line):
27 27 lines.append(line)
28 28 else:
29 29 lr.push(line)
30 30 break
31 31 return lines
32 32
33 33 while True:
34 34 line = lr.readline()
35 35 if not line:
36 36 break
37 37 if line.startswith('diff --git a/'):
38 38 def notheader(line):
39 39 s = line.split(None, 1)
40 40 return not s or s[0] not in ('---', 'diff')
41 41 header = scanwhile(line, notheader)
42 42 fromfile = lr.readline()
43 43 if fromfile.startswith('---'):
44 44 tofile = lr.readline()
45 45 header += [fromfile, tofile]
46 46 else:
47 47 lr.push(fromfile)
48 48 yield 'file', header
49 49 elif line[0] == ' ':
50 50 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
51 51 elif line[0] in '-+':
52 52 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
53 53 else:
54 54 m = lines_re.match(line)
55 55 if m:
56 56 yield 'range', m.groups()
57 57 else:
58 58 raise patch.PatchError('unknown patch content: %r' % line)
59 59
60 60 class header(object):
61 61 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
62 62 allhunks_re = re.compile('(?:index|new file|deleted file) ')
63 63 pretty_re = re.compile('(?:new file|deleted file) ')
64 64 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
65 65
66 66 def __init__(self, header):
67 67 self.header = header
68 68 self.hunks = []
69 69
70 70 def binary(self):
71 71 for h in self.header:
72 72 if h.startswith('index '):
73 73 return True
74
74
75 75 def pretty(self, fp):
76 76 for h in self.header:
77 77 if h.startswith('index '):
78 78 fp.write(_('this modifies a binary file (all or nothing)\n'))
79 79 break
80 80 if self.pretty_re.match(h):
81 81 fp.write(h)
82 82 if self.binary():
83 83 fp.write(_('this is a binary file\n'))
84 84 break
85 85 if h.startswith('---'):
86 86 fp.write(_('%d hunks, %d lines changed\n') %
87 87 (len(self.hunks),
88 88 sum([h.added + h.removed for h in self.hunks])))
89 89 break
90 90 fp.write(h)
91 91
92 92 def write(self, fp):
93 93 fp.write(''.join(self.header))
94 94
95 95 def allhunks(self):
96 96 for h in self.header:
97 97 if self.allhunks_re.match(h):
98 98 return True
99 99
100 100 def files(self):
101 101 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
102 102 if fromfile == tofile:
103 103 return [fromfile]
104 104 return [fromfile, tofile]
105 105
106 106 def filename(self):
107 107 return self.files()[-1]
108 108
109 109 def __repr__(self):
110 110 return '<header %s>' % (' '.join(map(repr, self.files())))
111 111
112 112 def special(self):
113 113 for h in self.header:
114 114 if self.special_re.match(h):
115 115 return True
116 116
117 117 def countchanges(hunk):
118 118 add = len([h for h in hunk if h[0] == '+'])
119 119 rem = len([h for h in hunk if h[0] == '-'])
120 120 return add, rem
121 121
122 122 class hunk(object):
123 123 maxcontext = 3
124 124
125 125 def __init__(self, header, fromline, toline, proc, before, hunk, after):
126 126 def trimcontext(number, lines):
127 127 delta = len(lines) - self.maxcontext
128 128 if False and delta > 0:
129 129 return number + delta, lines[:self.maxcontext]
130 130 return number, lines
131 131
132 132 self.header = header
133 133 self.fromline, self.before = trimcontext(fromline, before)
134 134 self.toline, self.after = trimcontext(toline, after)
135 135 self.proc = proc
136 136 self.hunk = hunk
137 137 self.added, self.removed = countchanges(self.hunk)
138 138
139 139 def write(self, fp):
140 140 delta = len(self.before) + len(self.after)
141 141 fromlen = delta + self.removed
142 142 tolen = delta + self.added
143 143 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
144 144 (self.fromline, fromlen, self.toline, tolen,
145 145 self.proc and (' ' + self.proc)))
146 146 fp.write(''.join(self.before + self.hunk + self.after))
147 147
148 148 pretty = write
149 149
150 150 def filename(self):
151 151 return self.header.filename()
152 152
153 153 def __repr__(self):
154 154 return '<hunk %r@%d>' % (self.filename(), self.fromline)
155 155
156 156 def parsepatch(fp):
157 157 class parser(object):
158 158 def __init__(self):
159 159 self.fromline = 0
160 160 self.toline = 0
161 161 self.proc = ''
162 162 self.header = None
163 163 self.context = []
164 164 self.before = []
165 165 self.hunk = []
166 166 self.stream = []
167 167
168 168 def addrange(self, (fromstart, fromend, tostart, toend, proc)):
169 169 self.fromline = int(fromstart)
170 170 self.toline = int(tostart)
171 171 self.proc = proc
172 172
173 173 def addcontext(self, context):
174 174 if self.hunk:
175 175 h = hunk(self.header, self.fromline, self.toline, self.proc,
176 176 self.before, self.hunk, context)
177 177 self.header.hunks.append(h)
178 178 self.stream.append(h)
179 179 self.fromline += len(self.before) + h.removed
180 180 self.toline += len(self.before) + h.added
181 181 self.before = []
182 182 self.hunk = []
183 183 self.proc = ''
184 184 self.context = context
185 185
186 186 def addhunk(self, hunk):
187 187 if self.context:
188 188 self.before = self.context
189 189 self.context = []
190 190 self.hunk = data
191 191
192 192 def newfile(self, hdr):
193 193 self.addcontext([])
194 194 h = header(hdr)
195 195 self.stream.append(h)
196 196 self.header = h
197 197
198 198 def finished(self):
199 199 self.addcontext([])
200 200 return self.stream
201 201
202 202 transitions = {
203 203 'file': {'context': addcontext,
204 204 'file': newfile,
205 205 'hunk': addhunk,
206 206 'range': addrange},
207 207 'context': {'file': newfile,
208 208 'hunk': addhunk,
209 209 'range': addrange},
210 210 'hunk': {'context': addcontext,
211 211 'file': newfile,
212 212 'range': addrange},
213 213 'range': {'context': addcontext,
214 214 'hunk': addhunk},
215 215 }
216
216
217 217 p = parser()
218 218
219 219 state = 'context'
220 220 for newstate, data in scanpatch(fp):
221 221 try:
222 222 p.transitions[state][newstate](p, data)
223 223 except KeyError:
224 224 raise patch.PatchError('unhandled transition: %s -> %s' %
225 225 (state, newstate))
226 226 state = newstate
227 227 return p.finished()
228 228
229 229 def filterpatch(ui, chunks):
230 230 chunks = list(chunks)
231 231 chunks.reverse()
232 232 seen = {}
233 233 def consumefile():
234 234 consumed = []
235 235 while chunks:
236 236 if isinstance(chunks[-1], header):
237 237 break
238 238 else:
239 239 consumed.append(chunks.pop())
240 240 return consumed
241 241 resp = None
242 242 applied = {}
243 243 while chunks:
244 244 chunk = chunks.pop()
245 245 if isinstance(chunk, header):
246 246 fixoffset = 0
247 247 hdr = ''.join(chunk.header)
248 248 if hdr in seen:
249 249 consumefile()
250 250 continue
251 251 seen[hdr] = True
252 252 if not resp:
253 253 chunk.pretty(ui)
254 254 r = resp or ui.prompt(_('record changes to %s? [y]es [n]o') %
255 255 _(' and ').join(map(repr, chunk.files())),
256 256 '(?:|[yYnNqQaA])$') or 'y'
257 257 if r in 'aA':
258 258 r = 'y'
259 259 resp = 'y'
260 260 if r in 'qQ':
261 261 raise util.Abort(_('user quit'))
262 262 if r in 'yY':
263 263 applied[chunk.filename()] = [chunk]
264 264 if chunk.allhunks():
265 265 applied[chunk.filename()] += consumefile()
266 266 else:
267 267 consumefile()
268 268 else:
269 269 if not resp:
270 270 chunk.pretty(ui)
271 271 r = resp or ui.prompt(_('record this change to %r? [y]es [n]o') %
272 272 chunk.filename(), '(?:|[yYnNqQaA])$') or 'y'
273 273 if r in 'aA':
274 274 r = 'y'
275 275 resp = 'y'
276 276 if r in 'qQ':
277 277 raise util.Abort(_('user quit'))
278 278 if r in 'yY':
279 279 if fixoffset:
280 280 chunk = copy.copy(chunk)
281 281 chunk.toline += fixoffset
282 282 applied[chunk.filename()].append(chunk)
283 283 else:
284 284 fixoffset += chunk.removed - chunk.added
285 285 return reduce(operator.add, [h for h in applied.itervalues()
286 286 if h[0].special() or len(h) > 1], [])
287 287
288 288 def record(ui, repo, *pats, **opts):
289 289 '''interactively select changes to commit'''
290 290
291 291 if not ui.interactive:
292 292 raise util.Abort(_('running non-interactively, use commit instead'))
293 293
294 294 def recordfunc(ui, repo, files, message, match, opts):
295 295 if files:
296 296 changes = None
297 297 else:
298 298 changes = repo.status(files=files, match=match)[:5]
299 299 modified, added, removed = changes[:3]
300 300 files = modified + added + removed
301 301 diffopts = mdiff.diffopts(git=True, nodates=True)
302 302 fp = cStringIO.StringIO()
303 303 patch.diff(repo, repo.dirstate.parents()[0], files=files,
304 304 match=match, changes=changes, opts=diffopts, fp=fp)
305 305 fp.seek(0)
306 306
307 307 chunks = filterpatch(ui, parsepatch(fp))
308 308 del fp
309 309
310 310 contenders = {}
311 311 for h in chunks:
312 312 try: contenders.update(dict.fromkeys(h.files()))
313 313 except AttributeError: pass
314
314
315 315 newfiles = [f for f in files if f in contenders]
316 316
317 317 if not newfiles:
318 318 ui.status(_('no changes to record\n'))
319 319 return 0
320 320
321 321 if changes is None:
322 322 changes = repo.status(files=newfiles, match=match)[:5]
323 323 modified = dict.fromkeys(changes[0])
324 324
325 325 backups = {}
326 326 backupdir = repo.join('record-backups')
327 327 try:
328 328 os.mkdir(backupdir)
329 329 except OSError, err:
330 330 if err.errno != errno.EEXIST:
331 331 raise
332 332 try:
333 333 for f in newfiles:
334 334 if f not in modified:
335 335 continue
336 336 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
337 337 dir=backupdir)
338 338 os.close(fd)
339 339 ui.debug('backup %r as %r\n' % (f, tmpname))
340 340 util.copyfile(repo.wjoin(f), tmpname)
341 341 backups[f] = tmpname
342 342
343 343 fp = cStringIO.StringIO()
344 344 for c in chunks:
345 345 if c.filename() in backups:
346 346 c.write(fp)
347 347 dopatch = fp.tell()
348 348 fp.seek(0)
349 349
350 350 if backups:
351 351 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
352 352
353 353 if dopatch:
354 354 ui.debug('applying patch\n')
355 355 ui.debug(fp.getvalue())
356 356 patch.internalpatch(fp, ui, 1, repo.root)
357 357 del fp
358 358
359 359 repo.commit(newfiles, message, opts['user'], opts['date'], match,
360 360 force_editor=opts.get('force_editor'))
361 361 return 0
362 362 finally:
363 363 try:
364 364 for realname, tmpname in backups.iteritems():
365 365 ui.debug('restoring %r to %r\n' % (tmpname, realname))
366 366 util.copyfile(tmpname, repo.wjoin(realname))
367 367 os.unlink(tmpname)
368 368 os.rmdir(backupdir)
369 369 except OSError:
370 370 pass
371 371 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
372 372
373 373 cmdtable = {
374 374 "record":
375 375 (record,
376 376 [('A', 'addremove', None,
377 377 _('mark new/missing files as added/removed before committing')),
378 378 ('d', 'date', '', _('record datecode as commit date')),
379 379 ('u', 'user', '', _('record user as commiter')),
380 380 ] + commands.walkopts + commands.commitopts,
381 381 _('hg record [OPTION]... [FILE]...')),
382 382 }
@@ -1,1330 +1,1330 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 from i18n import _
10 10 from node import *
11 11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
12 12 import cStringIO, email.Parser, os, popen2, re, sha
13 13 import sys, tempfile, zlib
14 14
15 15 class PatchError(Exception):
16 16 pass
17 17
18 18 class NoHunks(PatchError):
19 19 pass
20 20
21 21 # helper functions
22 22
23 23 def copyfile(src, dst, basedir=None):
24 24 if not basedir:
25 25 basedir = os.getcwd()
26 26
27 27 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
28 28 if os.path.exists(absdst):
29 29 raise util.Abort(_("cannot create %s: destination already exists") %
30 30 dst)
31 31
32 32 targetdir = os.path.dirname(absdst)
33 33 if not os.path.isdir(targetdir):
34 34 os.makedirs(targetdir)
35 35
36 36 util.copyfile(abssrc, absdst)
37 37
38 38 # public functions
39 39
40 40 def extract(ui, fileobj):
41 41 '''extract patch from data read from fileobj.
42 42
43 43 patch can be a normal patch or contained in an email message.
44 44
45 45 return tuple (filename, message, user, date, node, p1, p2).
46 46 Any item in the returned tuple can be None. If filename is None,
47 47 fileobj did not contain a patch. Caller must unlink filename when done.'''
48 48
49 49 # attempt to detect the start of a patch
50 50 # (this heuristic is borrowed from quilt)
51 51 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
52 52 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
53 53 '(---|\*\*\*)[ \t])', re.MULTILINE)
54 54
55 55 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
56 56 tmpfp = os.fdopen(fd, 'w')
57 57 try:
58 58 msg = email.Parser.Parser().parse(fileobj)
59 59
60 60 subject = msg['Subject']
61 61 user = msg['From']
62 62 # should try to parse msg['Date']
63 63 date = None
64 64 nodeid = None
65 65 branch = None
66 66 parents = []
67 67
68 68 if subject:
69 69 if subject.startswith('[PATCH'):
70 70 pend = subject.find(']')
71 71 if pend >= 0:
72 72 subject = subject[pend+1:].lstrip()
73 73 subject = subject.replace('\n\t', ' ')
74 74 ui.debug('Subject: %s\n' % subject)
75 75 if user:
76 76 ui.debug('From: %s\n' % user)
77 77 diffs_seen = 0
78 78 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
79 79 message = ''
80 80 for part in msg.walk():
81 81 content_type = part.get_content_type()
82 82 ui.debug('Content-Type: %s\n' % content_type)
83 83 if content_type not in ok_types:
84 84 continue
85 85 payload = part.get_payload(decode=True)
86 86 m = diffre.search(payload)
87 87 if m:
88 88 hgpatch = False
89 89 ignoretext = False
90 90
91 91 ui.debug(_('found patch at byte %d\n') % m.start(0))
92 92 diffs_seen += 1
93 93 cfp = cStringIO.StringIO()
94 94 for line in payload[:m.start(0)].splitlines():
95 95 if line.startswith('# HG changeset patch'):
96 96 ui.debug(_('patch generated by hg export\n'))
97 97 hgpatch = True
98 98 # drop earlier commit message content
99 99 cfp.seek(0)
100 100 cfp.truncate()
101 101 subject = None
102 102 elif hgpatch:
103 103 if line.startswith('# User '):
104 104 user = line[7:]
105 105 ui.debug('From: %s\n' % user)
106 106 elif line.startswith("# Date "):
107 107 date = line[7:]
108 108 elif line.startswith("# Branch "):
109 109 branch = line[9:]
110 110 elif line.startswith("# Node ID "):
111 111 nodeid = line[10:]
112 112 elif line.startswith("# Parent "):
113 113 parents.append(line[10:])
114 114 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
115 115 ignoretext = True
116 116 if not line.startswith('# ') and not ignoretext:
117 117 cfp.write(line)
118 118 cfp.write('\n')
119 119 message = cfp.getvalue()
120 120 if tmpfp:
121 121 tmpfp.write(payload)
122 122 if not payload.endswith('\n'):
123 123 tmpfp.write('\n')
124 124 elif not diffs_seen and message and content_type == 'text/plain':
125 125 message += '\n' + payload
126 126 except:
127 127 tmpfp.close()
128 128 os.unlink(tmpname)
129 129 raise
130 130
131 131 if subject and not message.startswith(subject):
132 132 message = '%s\n%s' % (subject, message)
133 133 tmpfp.close()
134 134 if not diffs_seen:
135 135 os.unlink(tmpname)
136 136 return None, message, user, date, branch, None, None, None
137 137 p1 = parents and parents.pop(0) or None
138 138 p2 = parents and parents.pop(0) or None
139 139 return tmpname, message, user, date, branch, nodeid, p1, p2
140 140
141 141 GP_PATCH = 1 << 0 # we have to run patch
142 142 GP_FILTER = 1 << 1 # there's some copy/rename operation
143 143 GP_BINARY = 1 << 2 # there's a binary patch
144 144
145 145 def readgitpatch(fp, firstline=None):
146 146 """extract git-style metadata about patches from <patchname>"""
147 147 class gitpatch:
148 148 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
149 149 def __init__(self, path):
150 150 self.path = path
151 151 self.oldpath = None
152 152 self.mode = None
153 153 self.op = 'MODIFY'
154 154 self.copymod = False
155 155 self.lineno = 0
156 156 self.binary = False
157 157
158 158 def reader(fp, firstline):
159 159 if firstline is not None:
160 160 yield firstline
161 161 for line in fp:
162 162 yield line
163 163
164 164 # Filter patch for git information
165 165 gitre = re.compile('diff --git a/(.*) b/(.*)')
166 166 gp = None
167 167 gitpatches = []
168 168 # Can have a git patch with only metadata, causing patch to complain
169 169 dopatch = 0
170 170
171 171 lineno = 0
172 172 for line in reader(fp, firstline):
173 173 lineno += 1
174 174 if line.startswith('diff --git'):
175 175 m = gitre.match(line)
176 176 if m:
177 177 if gp:
178 178 gitpatches.append(gp)
179 179 src, dst = m.group(1, 2)
180 180 gp = gitpatch(dst)
181 181 gp.lineno = lineno
182 182 elif gp:
183 183 if line.startswith('--- '):
184 184 if gp.op in ('COPY', 'RENAME'):
185 185 gp.copymod = True
186 186 dopatch |= GP_FILTER
187 187 gitpatches.append(gp)
188 188 gp = None
189 189 dopatch |= GP_PATCH
190 190 continue
191 191 if line.startswith('rename from '):
192 192 gp.op = 'RENAME'
193 193 gp.oldpath = line[12:].rstrip()
194 194 elif line.startswith('rename to '):
195 195 gp.path = line[10:].rstrip()
196 196 elif line.startswith('copy from '):
197 197 gp.op = 'COPY'
198 198 gp.oldpath = line[10:].rstrip()
199 199 elif line.startswith('copy to '):
200 200 gp.path = line[8:].rstrip()
201 201 elif line.startswith('deleted file'):
202 202 gp.op = 'DELETE'
203 203 elif line.startswith('new file mode '):
204 204 gp.op = 'ADD'
205 205 gp.mode = int(line.rstrip()[-6:], 8)
206 206 elif line.startswith('new mode '):
207 207 gp.mode = int(line.rstrip()[-6:], 8)
208 208 elif line.startswith('GIT binary patch'):
209 209 dopatch |= GP_BINARY
210 210 gp.binary = True
211 211 if gp:
212 212 gitpatches.append(gp)
213 213
214 214 if not gitpatches:
215 215 dopatch = GP_PATCH
216 216
217 217 return (dopatch, gitpatches)
218 218
219 219 def patch(patchname, ui, strip=1, cwd=None, files={}):
220 220 """apply <patchname> to the working directory.
221 221 returns whether patch was applied with fuzz factor."""
222 222 patcher = ui.config('ui', 'patch')
223 223 args = []
224 224 try:
225 225 if patcher:
226 226 return externalpatch(patcher, args, patchname, ui, strip, cwd,
227 227 files)
228 228 else:
229 229 try:
230 230 return internalpatch(patchname, ui, strip, cwd, files)
231 231 except NoHunks:
232 232 patcher = util.find_exe('gpatch') or util.find_exe('patch')
233 233 ui.debug('no valid hunks found; trying with %r instead\n' %
234 234 patcher)
235 235 if util.needbinarypatch():
236 236 args.append('--binary')
237 237 return externalpatch(patcher, args, patchname, ui, strip, cwd,
238 238 files)
239 239 except PatchError, err:
240 240 s = str(err)
241 241 if s:
242 242 raise util.Abort(s)
243 243 else:
244 244 raise util.Abort(_('patch failed to apply'))
245 245
246 246 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
247 247 """use <patcher> to apply <patchname> to the working directory.
248 248 returns whether patch was applied with fuzz factor."""
249 249
250 250 fuzz = False
251 251 if cwd:
252 252 args.append('-d %s' % util.shellquote(cwd))
253 253 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
254 254 util.shellquote(patchname)))
255 255
256 256 for line in fp:
257 257 line = line.rstrip()
258 258 ui.note(line + '\n')
259 259 if line.startswith('patching file '):
260 260 pf = util.parse_patch_output(line)
261 261 printed_file = False
262 262 files.setdefault(pf, (None, None))
263 263 elif line.find('with fuzz') >= 0:
264 264 fuzz = True
265 265 if not printed_file:
266 266 ui.warn(pf + '\n')
267 267 printed_file = True
268 268 ui.warn(line + '\n')
269 269 elif line.find('saving rejects to file') >= 0:
270 270 ui.warn(line + '\n')
271 271 elif line.find('FAILED') >= 0:
272 272 if not printed_file:
273 273 ui.warn(pf + '\n')
274 274 printed_file = True
275 275 ui.warn(line + '\n')
276 276 code = fp.close()
277 277 if code:
278 278 raise PatchError(_("patch command failed: %s") %
279 279 util.explain_exit(code)[0])
280 280 return fuzz
281 281
282 282 def internalpatch(patchobj, ui, strip, cwd, files={}):
283 283 """use builtin patch to apply <patchobj> to the working directory.
284 284 returns whether patch was applied with fuzz factor."""
285 285 try:
286 286 fp = file(patchobj, 'rb')
287 287 except TypeError:
288 288 fp = patchobj
289 289 if cwd:
290 290 curdir = os.getcwd()
291 291 os.chdir(cwd)
292 292 try:
293 293 ret = applydiff(ui, fp, files, strip=strip)
294 294 finally:
295 295 if cwd:
296 296 os.chdir(curdir)
297 297 if ret < 0:
298 298 raise PatchError
299 299 return ret > 0
300 300
301 301 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
302 302 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
303 303 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
304 304
305 305 class patchfile:
306 306 def __init__(self, ui, fname):
307 307 self.fname = fname
308 308 self.ui = ui
309 309 try:
310 310 fp = file(fname, 'rb')
311 311 self.lines = fp.readlines()
312 312 self.exists = True
313 313 except IOError:
314 314 dirname = os.path.dirname(fname)
315 315 if dirname and not os.path.isdir(dirname):
316 316 dirs = dirname.split(os.path.sep)
317 317 d = ""
318 318 for x in dirs:
319 319 d = os.path.join(d, x)
320 320 if not os.path.isdir(d):
321 321 os.mkdir(d)
322 322 self.lines = []
323 323 self.exists = False
324
324
325 325 self.hash = {}
326 326 self.dirty = 0
327 327 self.offset = 0
328 328 self.rej = []
329 329 self.fileprinted = False
330 330 self.printfile(False)
331 331 self.hunks = 0
332 332
333 333 def printfile(self, warn):
334 334 if self.fileprinted:
335 335 return
336 336 if warn or self.ui.verbose:
337 337 self.fileprinted = True
338 338 s = _("patching file %s\n") % self.fname
339 339 if warn:
340 340 self.ui.warn(s)
341 341 else:
342 342 self.ui.note(s)
343 343
344 344
345 345 def findlines(self, l, linenum):
346 346 # looks through the hash and finds candidate lines. The
347 347 # result is a list of line numbers sorted based on distance
348 348 # from linenum
349 349 def sorter(a, b):
350 350 vala = abs(a - linenum)
351 351 valb = abs(b - linenum)
352 352 return cmp(vala, valb)
353
353
354 354 try:
355 355 cand = self.hash[l]
356 356 except:
357 357 return []
358 358
359 359 if len(cand) > 1:
360 360 # resort our list of potentials forward then back.
361 361 cand.sort(cmp=sorter)
362 362 return cand
363 363
364 364 def hashlines(self):
365 365 self.hash = {}
366 366 for x in xrange(len(self.lines)):
367 367 s = self.lines[x]
368 368 self.hash.setdefault(s, []).append(x)
369 369
370 370 def write_rej(self):
371 371 # our rejects are a little different from patch(1). This always
372 372 # creates rejects in the same form as the original patch. A file
373 373 # header is inserted so that you can run the reject through patch again
374 374 # without having to type the filename.
375 375
376 376 if not self.rej:
377 377 return
378 378 if self.hunks != 1:
379 379 hunkstr = "s"
380 380 else:
381 381 hunkstr = ""
382 382
383 383 fname = self.fname + ".rej"
384 384 self.ui.warn(
385 385 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") %
386 386 (len(self.rej), self.hunks, hunkstr, fname))
387 387 try: os.unlink(fname)
388 388 except:
389 389 pass
390 390 fp = file(fname, 'wb')
391 391 base = os.path.basename(self.fname)
392 392 fp.write("--- %s\n+++ %s\n" % (base, base))
393 393 for x in self.rej:
394 394 for l in x.hunk:
395 395 fp.write(l)
396 396 if l[-1] != '\n':
397 397 fp.write("\n\ No newline at end of file\n")
398 398
399 399 def write(self, dest=None):
400 400 if self.dirty:
401 401 if not dest:
402 402 dest = self.fname
403 403 st = None
404 404 try:
405 405 st = os.lstat(dest)
406 406 if st.st_nlink > 1:
407 407 os.unlink(dest)
408 408 except: pass
409 409 fp = file(dest, 'wb')
410 410 if st:
411 411 os.chmod(dest, st.st_mode)
412 412 fp.writelines(self.lines)
413 413 fp.close()
414 414
415 415 def close(self):
416 416 self.write()
417 417 self.write_rej()
418 418
419 419 def apply(self, h, reverse):
420 420 if not h.complete():
421 421 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
422 422 (h.number, h.desc, len(h.a), h.lena, len(h.b),
423 423 h.lenb))
424 424
425 425 self.hunks += 1
426 426 if reverse:
427 427 h.reverse()
428 428
429 429 if self.exists and h.createfile():
430 430 self.ui.warn(_("file %s already exists\n") % self.fname)
431 431 self.rej.append(h)
432 432 return -1
433 433
434 434 if isinstance(h, binhunk):
435 435 if h.rmfile():
436 436 os.unlink(self.fname)
437 437 else:
438 438 self.lines[:] = h.new()
439 439 self.offset += len(h.new())
440 440 self.dirty = 1
441 441 return 0
442 442
443 443 # fast case first, no offsets, no fuzz
444 444 old = h.old()
445 445 # patch starts counting at 1 unless we are adding the file
446 446 if h.starta == 0:
447 447 start = 0
448 448 else:
449 449 start = h.starta + self.offset - 1
450 450 orig_start = start
451 451 if diffhelpers.testhunk(old, self.lines, start) == 0:
452 452 if h.rmfile():
453 453 os.unlink(self.fname)
454 454 else:
455 455 self.lines[start : start + h.lena] = h.new()
456 456 self.offset += h.lenb - h.lena
457 457 self.dirty = 1
458 458 return 0
459 459
460 460 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
461 461 self.hashlines()
462 462 if h.hunk[-1][0] != ' ':
463 463 # if the hunk tried to put something at the bottom of the file
464 464 # override the start line and use eof here
465 465 search_start = len(self.lines)
466 466 else:
467 467 search_start = orig_start
468 468
469 469 for fuzzlen in xrange(3):
470 470 for toponly in [ True, False ]:
471 471 old = h.old(fuzzlen, toponly)
472 472
473 473 cand = self.findlines(old[0][1:], search_start)
474 474 for l in cand:
475 475 if diffhelpers.testhunk(old, self.lines, l) == 0:
476 476 newlines = h.new(fuzzlen, toponly)
477 477 self.lines[l : l + len(old)] = newlines
478 478 self.offset += len(newlines) - len(old)
479 479 self.dirty = 1
480 480 if fuzzlen:
481 481 fuzzstr = "with fuzz %d " % fuzzlen
482 482 f = self.ui.warn
483 483 self.printfile(True)
484 484 else:
485 485 fuzzstr = ""
486 486 f = self.ui.note
487 487 offset = l - orig_start - fuzzlen
488 488 if offset == 1:
489 489 linestr = "line"
490 490 else:
491 491 linestr = "lines"
492 492 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
493 493 (h.number, l+1, fuzzstr, offset, linestr))
494 494 return fuzzlen
495 495 self.printfile(True)
496 496 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
497 497 self.rej.append(h)
498 498 return -1
499 499
500 500 class hunk:
501 501 def __init__(self, desc, num, lr, context):
502 502 self.number = num
503 503 self.desc = desc
504 504 self.hunk = [ desc ]
505 505 self.a = []
506 506 self.b = []
507 507 if context:
508 508 self.read_context_hunk(lr)
509 509 else:
510 510 self.read_unified_hunk(lr)
511 511
512 512 def read_unified_hunk(self, lr):
513 513 m = unidesc.match(self.desc)
514 514 if not m:
515 515 raise PatchError(_("bad hunk #%d") % self.number)
516 516 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
517 517 if self.lena == None:
518 518 self.lena = 1
519 519 else:
520 520 self.lena = int(self.lena)
521 521 if self.lenb == None:
522 522 self.lenb = 1
523 523 else:
524 524 self.lenb = int(self.lenb)
525 525 self.starta = int(self.starta)
526 526 self.startb = int(self.startb)
527 527 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
528 528 # if we hit eof before finishing out the hunk, the last line will
529 529 # be zero length. Lets try to fix it up.
530 530 while len(self.hunk[-1]) == 0:
531 531 del self.hunk[-1]
532 532 del self.a[-1]
533 533 del self.b[-1]
534 534 self.lena -= 1
535 535 self.lenb -= 1
536 536
537 537 def read_context_hunk(self, lr):
538 538 self.desc = lr.readline()
539 539 m = contextdesc.match(self.desc)
540 540 if not m:
541 541 raise PatchError(_("bad hunk #%d") % self.number)
542 542 foo, self.starta, foo2, aend, foo3 = m.groups()
543 543 self.starta = int(self.starta)
544 544 if aend == None:
545 545 aend = self.starta
546 546 self.lena = int(aend) - self.starta
547 547 if self.starta:
548 548 self.lena += 1
549 549 for x in xrange(self.lena):
550 550 l = lr.readline()
551 551 if l.startswith('---'):
552 552 lr.push(l)
553 553 break
554 554 s = l[2:]
555 555 if l.startswith('- ') or l.startswith('! '):
556 556 u = '-' + s
557 557 elif l.startswith(' '):
558 558 u = ' ' + s
559 559 else:
560 560 raise PatchError(_("bad hunk #%d old text line %d") %
561 561 (self.number, x))
562 562 self.a.append(u)
563 563 self.hunk.append(u)
564 564
565 565 l = lr.readline()
566 566 if l.startswith('\ '):
567 567 s = self.a[-1][:-1]
568 568 self.a[-1] = s
569 569 self.hunk[-1] = s
570 570 l = lr.readline()
571 571 m = contextdesc.match(l)
572 572 if not m:
573 573 raise PatchError(_("bad hunk #%d") % self.number)
574 574 foo, self.startb, foo2, bend, foo3 = m.groups()
575 575 self.startb = int(self.startb)
576 576 if bend == None:
577 577 bend = self.startb
578 578 self.lenb = int(bend) - self.startb
579 579 if self.startb:
580 580 self.lenb += 1
581 581 hunki = 1
582 582 for x in xrange(self.lenb):
583 583 l = lr.readline()
584 584 if l.startswith('\ '):
585 585 s = self.b[-1][:-1]
586 586 self.b[-1] = s
587 587 self.hunk[hunki-1] = s
588 588 continue
589 589 if not l:
590 590 lr.push(l)
591 591 break
592 592 s = l[2:]
593 593 if l.startswith('+ ') or l.startswith('! '):
594 594 u = '+' + s
595 595 elif l.startswith(' '):
596 596 u = ' ' + s
597 597 elif len(self.b) == 0:
598 598 # this can happen when the hunk does not add any lines
599 599 lr.push(l)
600 600 break
601 601 else:
602 602 raise PatchError(_("bad hunk #%d old text line %d") %
603 603 (self.number, x))
604 604 self.b.append(s)
605 605 while True:
606 606 if hunki >= len(self.hunk):
607 607 h = ""
608 608 else:
609 609 h = self.hunk[hunki]
610 610 hunki += 1
611 611 if h == u:
612 612 break
613 613 elif h.startswith('-'):
614 614 continue
615 615 else:
616 616 self.hunk.insert(hunki-1, u)
617 617 break
618 618
619 619 if not self.a:
620 620 # this happens when lines were only added to the hunk
621 621 for x in self.hunk:
622 622 if x.startswith('-') or x.startswith(' '):
623 623 self.a.append(x)
624 624 if not self.b:
625 625 # this happens when lines were only deleted from the hunk
626 626 for x in self.hunk:
627 627 if x.startswith('+') or x.startswith(' '):
628 628 self.b.append(x[1:])
629 629 # @@ -start,len +start,len @@
630 630 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
631 631 self.startb, self.lenb)
632 632 self.hunk[0] = self.desc
633 633
634 634 def reverse(self):
635 635 origlena = self.lena
636 636 origstarta = self.starta
637 637 self.lena = self.lenb
638 638 self.starta = self.startb
639 639 self.lenb = origlena
640 640 self.startb = origstarta
641 641 self.a = []
642 642 self.b = []
643 643 # self.hunk[0] is the @@ description
644 644 for x in xrange(1, len(self.hunk)):
645 645 o = self.hunk[x]
646 646 if o.startswith('-'):
647 647 n = '+' + o[1:]
648 648 self.b.append(o[1:])
649 649 elif o.startswith('+'):
650 650 n = '-' + o[1:]
651 651 self.a.append(n)
652 652 else:
653 653 n = o
654 654 self.b.append(o[1:])
655 655 self.a.append(o)
656 656 self.hunk[x] = o
657 657
658 658 def fix_newline(self):
659 659 diffhelpers.fix_newline(self.hunk, self.a, self.b)
660 660
661 661 def complete(self):
662 662 return len(self.a) == self.lena and len(self.b) == self.lenb
663 663
664 664 def createfile(self):
665 665 return self.starta == 0 and self.lena == 0
666 666
667 667 def rmfile(self):
668 668 return self.startb == 0 and self.lenb == 0
669 669
670 670 def fuzzit(self, l, fuzz, toponly):
671 671 # this removes context lines from the top and bottom of list 'l'. It
672 672 # checks the hunk to make sure only context lines are removed, and then
673 673 # returns a new shortened list of lines.
674 674 fuzz = min(fuzz, len(l)-1)
675 675 if fuzz:
676 676 top = 0
677 677 bot = 0
678 678 hlen = len(self.hunk)
679 679 for x in xrange(hlen-1):
680 680 # the hunk starts with the @@ line, so use x+1
681 681 if self.hunk[x+1][0] == ' ':
682 682 top += 1
683 683 else:
684 684 break
685 685 if not toponly:
686 686 for x in xrange(hlen-1):
687 687 if self.hunk[hlen-bot-1][0] == ' ':
688 688 bot += 1
689 689 else:
690 690 break
691 691
692 692 # top and bot now count context in the hunk
693 693 # adjust them if either one is short
694 694 context = max(top, bot, 3)
695 695 if bot < context:
696 696 bot = max(0, fuzz - (context - bot))
697 697 else:
698 698 bot = min(fuzz, bot)
699 699 if top < context:
700 700 top = max(0, fuzz - (context - top))
701 701 else:
702 702 top = min(fuzz, top)
703 703
704 704 return l[top:len(l)-bot]
705 705 return l
706 706
707 707 def old(self, fuzz=0, toponly=False):
708 708 return self.fuzzit(self.a, fuzz, toponly)
709
709
710 710 def newctrl(self):
711 711 res = []
712 712 for x in self.hunk:
713 713 c = x[0]
714 714 if c == ' ' or c == '+':
715 715 res.append(x)
716 716 return res
717 717
718 718 def new(self, fuzz=0, toponly=False):
719 719 return self.fuzzit(self.b, fuzz, toponly)
720 720
721 721 class binhunk:
722 722 'A binary patch file. Only understands literals so far.'
723 723 def __init__(self, gitpatch):
724 724 self.gitpatch = gitpatch
725 725 self.text = None
726 726 self.hunk = ['GIT binary patch\n']
727 727
728 728 def createfile(self):
729 729 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
730 730
731 731 def rmfile(self):
732 732 return self.gitpatch.op == 'DELETE'
733 733
734 734 def complete(self):
735 735 return self.text is not None
736 736
737 737 def new(self):
738 738 return [self.text]
739 739
740 740 def extract(self, fp):
741 741 line = fp.readline()
742 742 self.hunk.append(line)
743 743 while line and not line.startswith('literal '):
744 744 line = fp.readline()
745 745 self.hunk.append(line)
746 746 if not line:
747 747 raise PatchError(_('could not extract binary patch'))
748 748 size = int(line[8:].rstrip())
749 749 dec = []
750 750 line = fp.readline()
751 751 self.hunk.append(line)
752 752 while len(line) > 1:
753 753 l = line[0]
754 754 if l <= 'Z' and l >= 'A':
755 755 l = ord(l) - ord('A') + 1
756 756 else:
757 757 l = ord(l) - ord('a') + 27
758 758 dec.append(base85.b85decode(line[1:-1])[:l])
759 759 line = fp.readline()
760 760 self.hunk.append(line)
761 761 text = zlib.decompress(''.join(dec))
762 762 if len(text) != size:
763 763 raise PatchError(_('binary patch is %d bytes, not %d') %
764 764 len(text), size)
765 765 self.text = text
766 766
767 767 def parsefilename(str):
768 768 # --- filename \t|space stuff
769 769 s = str[4:]
770 770 i = s.find('\t')
771 771 if i < 0:
772 772 i = s.find(' ')
773 773 if i < 0:
774 774 return s
775 775 return s[:i]
776 776
777 777 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
778 778 def pathstrip(path, count=1):
779 779 pathlen = len(path)
780 780 i = 0
781 781 if count == 0:
782 782 return path.rstrip()
783 783 while count > 0:
784 784 i = path.find('/', i)
785 785 if i == -1:
786 786 raise PatchError(_("unable to strip away %d dirs from %s") %
787 787 (count, path))
788 788 i += 1
789 789 # consume '//' in the path
790 790 while i < pathlen - 1 and path[i] == '/':
791 791 i += 1
792 792 count -= 1
793 793 return path[i:].rstrip()
794 794
795 795 nulla = afile_orig == "/dev/null"
796 796 nullb = bfile_orig == "/dev/null"
797 797 afile = pathstrip(afile_orig, strip)
798 798 gooda = os.path.exists(afile) and not nulla
799 799 bfile = pathstrip(bfile_orig, strip)
800 800 if afile == bfile:
801 801 goodb = gooda
802 802 else:
803 803 goodb = os.path.exists(bfile) and not nullb
804 804 createfunc = hunk.createfile
805 805 if reverse:
806 806 createfunc = hunk.rmfile
807 807 if not goodb and not gooda and not createfunc():
808 808 raise PatchError(_("unable to find %s or %s for patching") %
809 809 (afile, bfile))
810 810 if gooda and goodb:
811 811 fname = bfile
812 812 if afile in bfile:
813 813 fname = afile
814 814 elif gooda:
815 815 fname = afile
816 816 elif not nullb:
817 817 fname = bfile
818 818 if afile in bfile:
819 819 fname = afile
820 820 elif not nulla:
821 821 fname = afile
822 822 return fname
823 823
824 824 class linereader:
825 825 # simple class to allow pushing lines back into the input stream
826 826 def __init__(self, fp):
827 827 self.fp = fp
828 828 self.buf = []
829 829
830 830 def push(self, line):
831 831 self.buf.append(line)
832 832
833 833 def readline(self):
834 834 if self.buf:
835 835 l = self.buf[0]
836 836 del self.buf[0]
837 837 return l
838 838 return self.fp.readline()
839 839
840 840 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
841 841 rejmerge=None, updatedir=None):
842 842 """reads a patch from fp and tries to apply it. The dict 'changed' is
843 843 filled in with all of the filenames changed by the patch. Returns 0
844 844 for a clean patch, -1 if any rejects were found and 1 if there was
845 any fuzz."""
845 any fuzz."""
846 846
847 847 def scangitpatch(fp, firstline, cwd=None):
848 848 '''git patches can modify a file, then copy that file to
849 849 a new file, but expect the source to be the unmodified form.
850 850 So we scan the patch looking for that case so we can do
851 851 the copies ahead of time.'''
852 852
853 853 pos = 0
854 854 try:
855 855 pos = fp.tell()
856 856 except IOError:
857 857 fp = cStringIO.StringIO(fp.read())
858 858
859 859 (dopatch, gitpatches) = readgitpatch(fp, firstline)
860 860 for gp in gitpatches:
861 861 if gp.copymod:
862 862 copyfile(gp.oldpath, gp.path, basedir=cwd)
863 863
864 864 fp.seek(pos)
865 865
866 866 return fp, dopatch, gitpatches
867 867
868 868 current_hunk = None
869 869 current_file = None
870 870 afile = ""
871 871 bfile = ""
872 872 state = None
873 873 hunknum = 0
874 874 rejects = 0
875 875
876 876 git = False
877 877 gitre = re.compile('diff --git (a/.*) (b/.*)')
878 878
879 879 # our states
880 880 BFILE = 1
881 881 err = 0
882 882 context = None
883 883 lr = linereader(fp)
884 884 dopatch = True
885 885 gitworkdone = False
886 886
887 887 while True:
888 888 newfile = False
889 889 x = lr.readline()
890 890 if not x:
891 891 break
892 892 if current_hunk:
893 893 if x.startswith('\ '):
894 894 current_hunk.fix_newline()
895 895 ret = current_file.apply(current_hunk, reverse)
896 896 if ret >= 0:
897 897 changed.setdefault(current_file.fname, (None, None))
898 898 if ret > 0:
899 899 err = 1
900 900 current_hunk = None
901 901 gitworkdone = False
902 902 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
903 903 ((context or context == None) and x.startswith('***************')))):
904 904 try:
905 905 if context == None and x.startswith('***************'):
906 906 context = True
907 907 current_hunk = hunk(x, hunknum + 1, lr, context)
908 908 except PatchError, err:
909 909 ui.debug(err)
910 910 current_hunk = None
911 911 continue
912 912 hunknum += 1
913 913 if not current_file:
914 914 if sourcefile:
915 915 current_file = patchfile(ui, sourcefile)
916 916 else:
917 917 current_file = selectfile(afile, bfile, current_hunk,
918 918 strip, reverse)
919 919 current_file = patchfile(ui, current_file)
920 920 elif state == BFILE and x.startswith('GIT binary patch'):
921 921 current_hunk = binhunk(changed[bfile[2:]][1])
922 922 if not current_file:
923 923 if sourcefile:
924 924 current_file = patchfile(ui, sourcefile)
925 925 else:
926 926 current_file = selectfile(afile, bfile, current_hunk,
927 927 strip, reverse)
928 928 current_file = patchfile(ui, current_file)
929 929 hunknum += 1
930 930 current_hunk.extract(fp)
931 931 elif x.startswith('diff --git'):
932 932 # check for git diff, scanning the whole patch file if needed
933 933 m = gitre.match(x)
934 934 if m:
935 935 afile, bfile = m.group(1, 2)
936 936 if not git:
937 937 git = True
938 938 fp, dopatch, gitpatches = scangitpatch(fp, x)
939 939 for gp in gitpatches:
940 940 changed[gp.path] = (gp.op, gp)
941 941 # else error?
942 942 # copy/rename + modify should modify target, not source
943 943 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
944 944 'RENAME'):
945 945 afile = bfile
946 946 gitworkdone = True
947 947 newfile = True
948 948 elif x.startswith('---'):
949 949 # check for a unified diff
950 950 l2 = lr.readline()
951 951 if not l2.startswith('+++'):
952 952 lr.push(l2)
953 953 continue
954 954 newfile = True
955 955 context = False
956 956 afile = parsefilename(x)
957 957 bfile = parsefilename(l2)
958 958 elif x.startswith('***'):
959 959 # check for a context diff
960 960 l2 = lr.readline()
961 961 if not l2.startswith('---'):
962 962 lr.push(l2)
963 963 continue
964 964 l3 = lr.readline()
965 965 lr.push(l3)
966 966 if not l3.startswith("***************"):
967 967 lr.push(l2)
968 968 continue
969 969 newfile = True
970 970 context = True
971 971 afile = parsefilename(x)
972 972 bfile = parsefilename(l2)
973 973
974 974 if newfile:
975 975 if current_file:
976 976 current_file.close()
977 977 if rejmerge:
978 978 rejmerge(current_file)
979 979 rejects += len(current_file.rej)
980 980 state = BFILE
981 981 current_file = None
982 982 hunknum = 0
983 983 if current_hunk:
984 984 if current_hunk.complete():
985 985 ret = current_file.apply(current_hunk, reverse)
986 986 if ret >= 0:
987 987 changed.setdefault(current_file.fname, (None, None))
988 988 if ret > 0:
989 989 err = 1
990 990 else:
991 991 fname = current_file and current_file.fname or None
992 992 raise PatchError(_("malformed patch %s %s") % (fname,
993 993 current_hunk.desc))
994 994 if current_file:
995 995 current_file.close()
996 996 if rejmerge:
997 997 rejmerge(current_file)
998 998 rejects += len(current_file.rej)
999 999 if updatedir and git:
1000 1000 updatedir(gitpatches)
1001 1001 if rejects:
1002 1002 return -1
1003 1003 if hunknum == 0 and dopatch and not gitworkdone:
1004 1004 raise NoHunks
1005 1005 return err
1006 1006
1007 1007 def diffopts(ui, opts={}, untrusted=False):
1008 1008 def get(key, name=None):
1009 1009 return (opts.get(key) or
1010 1010 ui.configbool('diff', name or key, None, untrusted=untrusted))
1011 1011 return mdiff.diffopts(
1012 1012 text=opts.get('text'),
1013 1013 git=get('git'),
1014 1014 nodates=get('nodates'),
1015 1015 showfunc=get('show_function', 'showfunc'),
1016 1016 ignorews=get('ignore_all_space', 'ignorews'),
1017 1017 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1018 1018 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
1019 1019
1020 1020 def updatedir(ui, repo, patches):
1021 1021 '''Update dirstate after patch application according to metadata'''
1022 1022 if not patches:
1023 1023 return
1024 1024 copies = []
1025 1025 removes = {}
1026 1026 cfiles = patches.keys()
1027 1027 cwd = repo.getcwd()
1028 1028 if cwd:
1029 1029 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1030 1030 for f in patches:
1031 1031 ctype, gp = patches[f]
1032 1032 if ctype == 'RENAME':
1033 1033 copies.append((gp.oldpath, gp.path, gp.copymod))
1034 1034 removes[gp.oldpath] = 1
1035 1035 elif ctype == 'COPY':
1036 1036 copies.append((gp.oldpath, gp.path, gp.copymod))
1037 1037 elif ctype == 'DELETE':
1038 1038 removes[gp.path] = 1
1039 1039 for src, dst, after in copies:
1040 1040 if not after:
1041 1041 copyfile(src, dst, repo.root)
1042 1042 repo.copy(src, dst)
1043 1043 removes = removes.keys()
1044 1044 if removes:
1045 1045 removes.sort()
1046 1046 repo.remove(removes, True)
1047 1047 for f in patches:
1048 1048 ctype, gp = patches[f]
1049 1049 if gp and gp.mode:
1050 1050 x = gp.mode & 0100 != 0
1051 1051 l = gp.mode & 020000 != 0
1052 1052 dst = os.path.join(repo.root, gp.path)
1053 1053 # patch won't create empty files
1054 1054 if ctype == 'ADD' and not os.path.exists(dst):
1055 1055 repo.wwrite(gp.path, '', x and 'x' or '')
1056 1056 else:
1057 1057 util.set_link(dst, l)
1058 1058 if not l:
1059 1059 util.set_exec(dst, x)
1060 1060 cmdutil.addremove(repo, cfiles)
1061 1061 files = patches.keys()
1062 1062 files.extend([r for r in removes if r not in files])
1063 1063 files.sort()
1064 1064
1065 1065 return files
1066 1066
1067 1067 def b85diff(to, tn):
1068 1068 '''print base85-encoded binary diff'''
1069 1069 def gitindex(text):
1070 1070 if not text:
1071 1071 return '0' * 40
1072 1072 l = len(text)
1073 1073 s = sha.new('blob %d\0' % l)
1074 1074 s.update(text)
1075 1075 return s.hexdigest()
1076 1076
1077 1077 def fmtline(line):
1078 1078 l = len(line)
1079 1079 if l <= 26:
1080 1080 l = chr(ord('A') + l - 1)
1081 1081 else:
1082 1082 l = chr(l - 26 + ord('a') - 1)
1083 1083 return '%c%s\n' % (l, base85.b85encode(line, True))
1084 1084
1085 1085 def chunk(text, csize=52):
1086 1086 l = len(text)
1087 1087 i = 0
1088 1088 while i < l:
1089 1089 yield text[i:i+csize]
1090 1090 i += csize
1091 1091
1092 1092 tohash = gitindex(to)
1093 1093 tnhash = gitindex(tn)
1094 1094 if tohash == tnhash:
1095 1095 return ""
1096 1096
1097 1097 # TODO: deltas
1098 1098 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1099 1099 (tohash, tnhash, len(tn))]
1100 1100 for l in chunk(zlib.compress(tn)):
1101 1101 ret.append(fmtline(l))
1102 1102 ret.append('\n')
1103 1103 return ''.join(ret)
1104 1104
1105 1105 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1106 1106 fp=None, changes=None, opts=None):
1107 1107 '''print diff of changes to files between two nodes, or node and
1108 1108 working directory.
1109 1109
1110 1110 if node1 is None, use first dirstate parent instead.
1111 1111 if node2 is None, compare node1 with working directory.'''
1112 1112
1113 1113 if opts is None:
1114 1114 opts = mdiff.defaultopts
1115 1115 if fp is None:
1116 1116 fp = repo.ui
1117 1117
1118 1118 if not node1:
1119 1119 node1 = repo.dirstate.parents()[0]
1120 1120
1121 1121 ccache = {}
1122 1122 def getctx(r):
1123 1123 if r not in ccache:
1124 1124 ccache[r] = context.changectx(repo, r)
1125 1125 return ccache[r]
1126 1126
1127 1127 flcache = {}
1128 1128 def getfilectx(f, ctx):
1129 1129 flctx = ctx.filectx(f, filelog=flcache.get(f))
1130 1130 if f not in flcache:
1131 1131 flcache[f] = flctx._filelog
1132 1132 return flctx
1133 1133
1134 1134 # reading the data for node1 early allows it to play nicely
1135 1135 # with repo.status and the revlog cache.
1136 1136 ctx1 = context.changectx(repo, node1)
1137 1137 # force manifest reading
1138 1138 man1 = ctx1.manifest()
1139 1139 date1 = util.datestr(ctx1.date())
1140 1140
1141 1141 if not changes:
1142 1142 changes = repo.status(node1, node2, files, match=match)[:5]
1143 1143 modified, added, removed, deleted, unknown = changes
1144 1144
1145 1145 if not modified and not added and not removed:
1146 1146 return
1147 1147
1148 1148 if node2:
1149 1149 ctx2 = context.changectx(repo, node2)
1150 1150 execf2 = ctx2.manifest().execf
1151 1151 linkf2 = ctx2.manifest().linkf
1152 1152 else:
1153 1153 ctx2 = context.workingctx(repo)
1154 1154 execf2 = util.execfunc(repo.root, None)
1155 1155 linkf2 = util.linkfunc(repo.root, None)
1156 1156 if execf2 is None:
1157 1157 mc = ctx2.parents()[0].manifest().copy()
1158 1158 execf2 = mc.execf
1159 1159 linkf2 = mc.linkf
1160 1160
1161 1161 # returns False if there was no rename between ctx1 and ctx2
1162 1162 # returns None if the file was created between ctx1 and ctx2
1163 1163 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1164 1164 def renamed(f):
1165 1165 startrev = ctx1.rev()
1166 1166 c = ctx2
1167 1167 crev = c.rev()
1168 1168 if crev is None:
1169 1169 crev = repo.changelog.count()
1170 1170 orig = f
1171 1171 while crev > startrev:
1172 1172 if f in c.files():
1173 1173 try:
1174 1174 src = getfilectx(f, c).renamed()
1175 1175 except revlog.LookupError:
1176 1176 return None
1177 1177 if src:
1178 1178 f = src[0]
1179 1179 crev = c.parents()[0].rev()
1180 1180 # try to reuse
1181 1181 c = getctx(crev)
1182 1182 if f not in man1:
1183 1183 return None
1184 1184 if f == orig:
1185 1185 return False
1186 1186 return f
1187 1187
1188 1188 if repo.ui.quiet:
1189 1189 r = None
1190 1190 else:
1191 1191 hexfunc = repo.ui.debugflag and hex or short
1192 1192 r = [hexfunc(node) for node in [node1, node2] if node]
1193 1193
1194 1194 if opts.git:
1195 1195 copied = {}
1196 1196 for f in added:
1197 1197 src = renamed(f)
1198 1198 if src:
1199 1199 copied[f] = src
1200 1200 srcs = [x[1] for x in copied.items()]
1201 1201
1202 1202 all = modified + added + removed
1203 1203 all.sort()
1204 1204 gone = {}
1205 1205
1206 1206 for f in all:
1207 1207 to = None
1208 1208 tn = None
1209 1209 dodiff = True
1210 1210 header = []
1211 1211 if f in man1:
1212 1212 to = getfilectx(f, ctx1).data()
1213 1213 if f not in removed:
1214 1214 tn = getfilectx(f, ctx2).data()
1215 1215 if opts.git:
1216 1216 def gitmode(x, l):
1217 1217 return l and '120000' or (x and '100755' or '100644')
1218 1218 def addmodehdr(header, omode, nmode):
1219 1219 if omode != nmode:
1220 1220 header.append('old mode %s\n' % omode)
1221 1221 header.append('new mode %s\n' % nmode)
1222 1222
1223 1223 a, b = f, f
1224 1224 if f in added:
1225 1225 mode = gitmode(execf2(f), linkf2(f))
1226 1226 if f in copied:
1227 1227 a = copied[f]
1228 1228 omode = gitmode(man1.execf(a), man1.linkf(a))
1229 1229 addmodehdr(header, omode, mode)
1230 1230 if a in removed and a not in gone:
1231 1231 op = 'rename'
1232 1232 gone[a] = 1
1233 1233 else:
1234 1234 op = 'copy'
1235 1235 header.append('%s from %s\n' % (op, a))
1236 1236 header.append('%s to %s\n' % (op, f))
1237 1237 to = getfilectx(a, ctx1).data()
1238 1238 else:
1239 1239 header.append('new file mode %s\n' % mode)
1240 1240 if util.binary(tn):
1241 1241 dodiff = 'binary'
1242 1242 elif f in removed:
1243 1243 if f in srcs:
1244 1244 dodiff = False
1245 1245 else:
1246 1246 mode = gitmode(man1.execf(f), man1.linkf(f))
1247 1247 header.append('deleted file mode %s\n' % mode)
1248 1248 else:
1249 1249 omode = gitmode(man1.execf(f), man1.linkf(f))
1250 1250 nmode = gitmode(execf2(f), linkf2(f))
1251 1251 addmodehdr(header, omode, nmode)
1252 1252 if util.binary(to) or util.binary(tn):
1253 1253 dodiff = 'binary'
1254 1254 r = None
1255 1255 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1256 1256 if dodiff:
1257 1257 if dodiff == 'binary':
1258 1258 text = b85diff(to, tn)
1259 1259 else:
1260 1260 text = mdiff.unidiff(to, date1,
1261 1261 # ctx2 date may be dynamic
1262 1262 tn, util.datestr(ctx2.date()),
1263 1263 f, r, opts=opts)
1264 1264 if text or len(header) > 1:
1265 1265 fp.write(''.join(header))
1266 1266 fp.write(text)
1267 1267
1268 1268 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1269 1269 opts=None):
1270 1270 '''export changesets as hg patches.'''
1271 1271
1272 1272 total = len(revs)
1273 1273 revwidth = max([len(str(rev)) for rev in revs])
1274 1274
1275 1275 def single(rev, seqno, fp):
1276 1276 ctx = repo.changectx(rev)
1277 1277 node = ctx.node()
1278 1278 parents = [p.node() for p in ctx.parents() if p]
1279 1279 branch = ctx.branch()
1280 1280 if switch_parent:
1281 1281 parents.reverse()
1282 1282 prev = (parents and parents[0]) or nullid
1283 1283
1284 1284 if not fp:
1285 1285 fp = cmdutil.make_file(repo, template, node, total=total,
1286 1286 seqno=seqno, revwidth=revwidth)
1287 1287 if fp != sys.stdout and hasattr(fp, 'name'):
1288 1288 repo.ui.note("%s\n" % fp.name)
1289 1289
1290 1290 fp.write("# HG changeset patch\n")
1291 1291 fp.write("# User %s\n" % ctx.user())
1292 1292 fp.write("# Date %d %d\n" % ctx.date())
1293 1293 if branch and (branch != 'default'):
1294 1294 fp.write("# Branch %s\n" % branch)
1295 1295 fp.write("# Node ID %s\n" % hex(node))
1296 1296 fp.write("# Parent %s\n" % hex(prev))
1297 1297 if len(parents) > 1:
1298 1298 fp.write("# Parent %s\n" % hex(parents[1]))
1299 1299 fp.write(ctx.description().rstrip())
1300 1300 fp.write("\n\n")
1301 1301
1302 1302 diff(repo, prev, node, fp=fp, opts=opts)
1303 1303 if fp not in (sys.stdout, repo.ui):
1304 1304 fp.close()
1305 1305
1306 1306 for seqno, rev in enumerate(revs):
1307 1307 single(rev, seqno+1, fp)
1308 1308
1309 1309 def diffstat(patchlines):
1310 1310 if not util.find_exe('diffstat'):
1311 1311 return
1312 1312 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1313 1313 try:
1314 1314 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1315 1315 try:
1316 1316 for line in patchlines: print >> p.tochild, line
1317 1317 p.tochild.close()
1318 1318 if p.wait(): return
1319 1319 fp = os.fdopen(fd, 'r')
1320 1320 stat = []
1321 1321 for line in fp: stat.append(line.lstrip())
1322 1322 last = stat.pop()
1323 1323 stat.insert(0, last)
1324 1324 stat = ''.join(stat)
1325 1325 if stat.startswith('0 files'): raise ValueError
1326 1326 return stat
1327 1327 except: raise
1328 1328 finally:
1329 1329 try: os.unlink(name)
1330 1330 except: pass
@@ -1,40 +1,40 b''
1 1 #!/bin/sh
2 2
3 3 echo "[extensions]" >> $HGRCPATH
4 4 echo "extdiff=" >> $HGRCPATH
5 5
6 6 hg init a
7 7 cd a
8 8 echo a > a
9 9 echo b > b
10 10 hg add
11 11 # should diff cloned directories
12 12 hg extdiff -o -r $opt
13 13
14 14 echo "[extdiff]" >> $HGRCPATH
15 15 echo "cmd.falabala=echo" >> $HGRCPATH
16 16 echo "opts.falabala=diffing" >> $HGRCPATH
17 17
18 18 hg falabala
19 19
20 20 hg help falabala
21 21
22 22 hg ci -d '0 0' -mtest1
23 23
24 24 echo b >> a
25 25 hg ci -d '1 0' -mtest2
26 26
27 # should diff cloned files directly
27 # should diff cloned files directly
28 28 hg falabala -r 0:1
29 29
30 30 # test diff during merge
31 31 hg update 0
32 32 echo c >> c
33 33 hg add c
34 34 hg ci -m "new branch" -d '1 0'
35 35 hg update -C 1
36 36 hg merge tip
37 # should diff cloned file against wc file
37 # should diff cloned file against wc file
38 38 hg falabala > out || echo "diff-like tools yield a non-zero exit code"
39 39 # cleanup the output since the wc is a tmp directory
40 40 sed 's:\(.* \).*\(\/test-extdiff\):\1[tmp]\2:' out
General Comments 0
You need to be logged in to leave comments. Login now