##// END OF EJS Templates
diffs: compare overhaul....
marcink -
r1259:8e9f93ec default
parent child Browse files
Show More
@@ -0,0 +1,9 b''
1 ===================================================================
2 Cannot display: file marked as a binary type.
3 svn:mime-type = application/octet-stream
4 Index: intl.dll
5 ===================================================================
6 diff --git a/intl.dll b/intl.dll
7 new file mode 10644
8 --- /dev/null (revision 0)
9 +++ b/intl.dll (revision 1489)
This diff has been collapsed as it changes many lines, (652 lines changed) Show them Hide them
@@ -0,0 +1,652 b''
1 ===================================================================
2 Cannot display: file marked as a binary type.
3 svn:mime-type = image/png
4 Index: trunk/doc/images/SettingsOverlay.png
5 ===================================================================
6 diff --git a/trunk/doc/images/SettingsOverlay.png b/trunk/doc/images/SettingsOverlay.png
7 GIT binary patch
8 --- a/trunk/doc/images/SettingsOverlay.png (revision 1487)
9 +++ b/trunk/doc/images/SettingsOverlay.png (revision 1488)
10 Index: trunk/doc/source/de/tsvn_ch04.xml
11 ===================================================================
12 diff --git a/trunk/doc/source/de/tsvn_ch04.xml b/trunk/doc/source/de/tsvn_ch04.xml
13 --- a/trunk/doc/source/de/tsvn_ch04.xml (revision 1487)
14 +++ b/trunk/doc/source/de/tsvn_ch04.xml (revision 1488)
15 @@ -1561,39 +1561,49 @@
16 </figure>
17 Abgesehen von der bevorzugten Sprache erlaubt dieser Dialog es Ihnen,
18 (fast) alle Einstellungen von TortoiseSVN zu ändern.
19 -### Translate ###
20 <variablelist>
21 <varlistentry>
22 - <term>Language</term>
23 - <listitem>
24 - <para>Selects your user interface language. What did you expect?</para>
25 + <term>Sprache</term>
26 + <listitem>
27 + <para>Wählt die Sprache für die Dialoge/Meldungen aus. Was
28 + haben Sie anderes erwartet?</para>
29 </listitem>
30 </varlistentry>
31
32 <varlistentry>
33 - <term>Exclude pattern</term>
34 + <term>Ausschliessen</term>
35 <listitem>
36 <para>
37 <indexterm>
38 - <primary>exclude pattern</primary>
39 + <primary>ausschliessen</primary>
40 </indexterm>
41 - Exclude files or directories by typing in the names or extensions. Patterns are separated by spaces
42 - e.g. <literal>bin obj *.bak *.~?? *.jar *.[Tt]mp</literal>. The first two entries refer to directories, the
43 - other four to files.
44 - </para>
45 - <para>
46 - This exclude pattern will affect all your projects. It is not versioned, so it
47 - will not affect other users. In contrast you can also use the versioned svn:ignore
48 - property to exclude files or directories from version control. You can set the svn:ignore
49 - property using the
50 + Ausgeschlossene, unversionierte Dateien werden nicht angezeigt
51 + in z.B. dem Ãœbertragen Dialog. Ausserdem werden solche Dateien
52 + beim Importieren in ein Projektarchiv ignoriert.
53 + Schliessen Sie Dateien oder Ordner aus durch Angabe von
54 + Dateinamen oder Erweiterungen. Die einzelnen Muster werden
55 + durch Leerzeichen voneinander getrennt. Zum Beispiel
56 + <literal>bin obj *.bak *.~?? *.jar *.[Tt]mp</literal>.
57 + Die ersten beiden Muster beziehen sich auf Ordner, die
58 + restlichen vier auf Dateien.
59 + </para>
60 + <para>
61 + Diese Auschluss-Muster beziehen sich auf alle Ihre Projekte.
62 + Sie werden nicht versioniert, d.h. andere Benutzer werden davon
63 + nichts mitbekommen. Im Gegensatz dazu können Sie jedoch auch
64 + die versionierte Eigenschaft svn:ignore verwenden, um Dateien
65 + und/oder Ordner von der Versionskontrolle auszuschliessen.
66 + Sie können die svn:ignore Eigenschaft setzen durch den
67 <menuchoice>
68 - <guimenuitem>Add to Ignore List</guimenuitem>
69 + <guimenuitem>Ignorieren</guimenuitem>
70 </menuchoice>
71 - command. After commiting every other user will have the same
72 - svn:ignore property set for this project / directory as you.
73 + Befehl. Nach dem Ãœbertragen wird jeder Benutzer dieselbe
74 + svn:ignore Eigenschaft für das Projekt oder den Ordner
75 + haben wie Sie.
76 </para>
77 </listitem>
78 </varlistentry>
79 +### Translate ###
80
81 <varlistentry>
82 <term>Default number of log messages</term>
83 @@ -1608,16 +1618,36 @@
84 </varlistentry>
85
86 <varlistentry>
87 - <term>Short date / time format in log messages</term>
88 - <listitem>
89 - <para>If the standard long messages use up too much space on your sceen use the short format.</para>
90 + <term>Edit...</term>
91 + <listitem>
92 + <para>... the subversion configuration file directly. Some settings cannot be modified by TortoiseSVN.</para>
93 </listitem>
94 </varlistentry>
95
96 <varlistentry>
97 - <term>Edit...</term>
98 - <listitem>
99 - <para>... the subversion configuration file directly. Some settings cannot be modified by TortoiseSVN.</para>
100 + <term>Short date / time format in log messages</term>
101 + <listitem>
102 + <para>If the standard long messages use up too much space on your sceen use the short format.</para>
103 + </listitem>
104 + </varlistentry>
105 +
106 + <varlistentry>
107 + <term>Set filedates to "last commit time"</term>
108 + <listitem>
109 + <para>
110 + This option tells TortoiseSVN to set the filedates to the last commit time
111 + when doing a checkout or an update. Otherwise TortoiseSVN will use
112 + the current date.
113 + </para>
114 + </listitem>
115 + </varlistentry>
116 +
117 + <varlistentry>
118 + <term>Close windows automatically</term>
119 + <listitem>
120 + <para>
121 + TortoiseSVN will automatically close all progress dialogs when the action is finished.
122 + </para>
123 </listitem>
124 </varlistentry>
125
126 @@ -1629,15 +1659,15 @@
127 </varlistentry>
128
129 <varlistentry>
130 - <term>Set filedates to "last commit time"</term>
131 - <listitem>
132 - <para>
133 - This option tells TortoiseSVN to set the filedates to the last commit time
134 - when doing a checkout or an update. Otherwise TortoiseSVN will use
135 - the current date.
136 + <term>Minimum logsize in chars</term>
137 + <listitem>
138 + <para>
139 + The minimum length of a log message for a commit. If you enter
140 + a shorter message than specified here, the commit is disabled.
141 </para>
142 </listitem>
143 </varlistentry>
144 +
145 <varlistentry>
146 <term>Don't remove log messages when cancelling a commit</term>
147 <listitem>
148 @@ -1648,11 +1678,14 @@
149 </para>
150 </listitem>
151 </varlistentry>
152 +
153 <varlistentry>
154 - <term>Close windows automatically</term>
155 - <listitem>
156 - <para>
157 - TortoiseSVN will automatically close all progress dialogs when the action is finished.
158 + <term>Show BugID/Issue-Nr. Box</term>
159 + <listitem>
160 + <para>
161 + Shows a textbox in the commit dialog where you can enter
162 + a BugID or Issue-Nr. from a bugtracker to associate the
163 + commit with that ID/number.
164 </para>
165 </listitem>
166 </varlistentry>
167 @@ -1673,10 +1706,32 @@
168 Sie können auch alle überlagerten Icons deaktivieren, aber wo liegt der Spaß darin?
169 </para>
170 <para>
171 + Die <term>Ausschluss Pfade</term> sagen TortoiseSVN für welche
172 + Pfade die überlagerten Icons <emphasis>nicht</emphasis> gezeichnet
173 + werden sollen. Dies ist nützlich wenn Sie zum Beispiel sehr grosse
174 + Arbeitskopien haben, welche grosse externe Bibliotheken, welche Sie
175 + selbst nie ändern werden enthalten. Sie können dann diese Pfade
176 + ausschliessen. Zum Beispiel:
177 + </para>
178 + <para>
179 + <filename>f:\development\SVN\Subversion</filename> deaktiviert
180 + die überlagerten Icons <emphasis>nur</emphasis> für diesen speziellen
181 + Ordner. Sie können die Icons noch immer für alle Dateien und Ordner
182 + innerhalb sehen.
183 + </para>
184 + <para>
185 + <filename>f:\development\SVN\Subversion*</filename> deaktiviert die
186 + überlagerten Icons für <emphasis>alle</emphasis> Dateien und Ordner
187 + welcher Pfad mit <filename>f:\development\SVN\Subversion</filename>
188 + beginnt. Das bedeutet dass auch für alle Dateien und Ordner innerhalb
189 + keine überlagerten Icons angezeigt werden.
190 + </para>
191 + <para>
192 Ausserdem können Sie angeben, welche Befehle im
193 Hauptkontextmenu des Explorer angezeigt werden sollen und welche
194 Sie lieber im Untermenu haben wollen.
195 </para>
196 + </sect2>
197 <sect2 id="tsvn-DUG-settings-network">
198 <?dbhh topicname="HIDD_SETTINGSPROXY"?>
199 <title>Der Einstellungsdialog, Netzwerkseite</title>
200 Index: trunk/doc/source/en/tsvn_ch04.xml
201 ===================================================================
202 diff --git a/trunk/doc/source/en/tsvn_ch04.xml b/trunk/doc/source/en/tsvn_ch04.xml
203 --- a/trunk/doc/source/en/tsvn_ch04.xml (revision 1487)
204 +++ b/trunk/doc/source/en/tsvn_ch04.xml (revision 1488)
205 @@ -1457,7 +1457,7 @@
206 <varlistentry>
207 <term>Language</term>
208 <listitem>
209 - <para>Selects your user interface language. What did you expect?</para>
210 + <para>Selects your user interface language. What else did you expect?</para>
211 </listitem>
212 </varlistentry>
213
214 @@ -1468,6 +1468,9 @@
215 <indexterm>
216 <primary>exclude pattern</primary>
217 </indexterm>
218 + Exclude patterns are used to prevent unversioned files from
219 + showing up e.g. in the commit dialog. Files matching the
220 + patterns are also ignored by an import.
221 Exclude files or directories by typing in the names or extensions. Patterns are separated by spaces
222 e.g. <literal>bin obj *.bak *.~?? *.jar *.[Tt]mp</literal>. The first two entries refer to directories, the
223 other four to files.
224 @@ -1499,23 +1502,16 @@
225 </varlistentry>
226
227 <varlistentry>
228 + <term>Edit...</term>
229 + <listitem>
230 + <para>... the subversion configuration file directly. Some settings cannot be modified by TortoiseSVN.</para>
231 + </listitem>
232 + </varlistentry>
233 +
234 + <varlistentry>
235 <term>Short date / time format in log messages</term>
236 <listitem>
237 <para>If the standard long messages use up too much space on your sceen use the short format.</para>
238 - </listitem>
239 - </varlistentry>
240 -
241 - <varlistentry>
242 - <term>Edit...</term>
243 - <listitem>
244 - <para>... the subversion configuration file directly. Some settings cannot be modified by TortoiseSVN.</para>
245 - </listitem>
246 - </varlistentry>
247 -
248 - <varlistentry>
249 - <term>Check for newer versions</term>
250 - <listitem>
251 - <para>If checked, TortoiseSVN will check once a week if an update is available</para>
252 </listitem>
253 </varlistentry>
254
255 @@ -1529,6 +1525,33 @@
256 </para>
257 </listitem>
258 </varlistentry>
259 +
260 + <varlistentry>
261 + <term>Close windows automatically</term>
262 + <listitem>
263 + <para>
264 + TortoiseSVN will automatically close all progress dialogs when the action is finished.
265 + </para>
266 + </listitem>
267 + </varlistentry>
268 +
269 + <varlistentry>
270 + <term>Check for newer versions</term>
271 + <listitem>
272 + <para>If checked, TortoiseSVN will check once a week if an update is available</para>
273 + </listitem>
274 + </varlistentry>
275 +
276 + <varlistentry>
277 + <term>Minimum logsize in chars</term>
278 + <listitem>
279 + <para>
280 + The minimum length of a log message for a commit. If you enter
281 + a shorter message than specified here, the commit is disabled.
282 + </para>
283 + </listitem>
284 + </varlistentry>
285 +
286 <varlistentry>
287 <term>Don't remove log messages when cancelling a commit</term>
288 <listitem>
289 @@ -1539,11 +1562,14 @@
290 </para>
291 </listitem>
292 </varlistentry>
293 +
294 <varlistentry>
295 - <term>Close windows automatically</term>
296 - <listitem>
297 - <para>
298 - TortoiseSVN will automatically close all progress dialogs when the action is finished.
299 + <term>Show BugID/Issue-Nr. Box</term>
300 + <listitem>
301 + <para>
302 + Shows a textbox in the commit dialog where you can enter
303 + a BugID or Issue-Nr. from a bugtracker to associate the
304 + commit with that ID/number.
305 </para>
306 </listitem>
307 </varlistentry>
308 @@ -1552,7 +1578,7 @@
309 </sect2>
310 <sect2 id="tsvn-DUG-settings-overlay">
311 <?dbhh topicname="HIDD_SETTINGSOVERLAY"?>
312 - <title>The Settings Dialog, Overlay Tab</title>
313 + <title>The Settings Dialog, Look and Feel Tab</title>
314 <para>
315 <figure id="tsvn-DUG-settings-dia-2">
316 <title>The Settings Dialog, Overlay Tab</title>
317 @@ -1560,8 +1586,27 @@
318 </figure>
319 This tab allows you to choose, for which items TortoiseSVN shall
320 display icon overlays. If you feel that your icon overlays are very
321 - slow (explore is not responsive), uncheck the "show changed directories" box.
322 + slow (explorer is not responsive), uncheck the "show changed directories" box.
323 You can even disable all icon overlays, but where's the fun in that?
324 + </para>
325 + <para>
326 + The <term>Exclude Paths</term> are used to tell TortoiseSVN for which
327 + paths <emphasis>not</emphasis> to show icon overlays and status columns.
328 + This is useful if you have some very big working copies containing
329 + only libraries which you won't change at all and therefore don't
330 + need the overlays. For example:
331 + </para>
332 + <para>
333 + <filename>f:\development\SVN\Subversion</filename> will disable
334 + the overlays on <emphasis>only</emphasis> that specific folder. You
335 + still can see the overlays on all files and folder inside that folder.
336 + </para>
337 + <para>
338 + <filename>f:\development\SVN\Subversion*</filename> will disable the
339 + overlays on <emphasis>all</emphasis> files and folders which path
340 + starts with <filename>f:\development\SVN\Subversion</filename>. That
341 + means you won't see overlays for all files and folder below that
342 + path.
343 </para>
344 <para>
345 You can also specifiy here which of the TortoiseSVN contex menu
346 Index: trunk/src/Changelog.txt
347 ===================================================================
348 diff --git a/trunk/src/Changelog.txt b/trunk/src/Changelog.txt
349 --- a/trunk/src/Changelog.txt (revision 1487)
350 +++ b/trunk/src/Changelog.txt (revision 1488)
351 @@ -1,3 +1,5 @@
352 +- ADD: Option to exclude specific paths from showing
353 + icon overlays. (Stefan)
354 - ADD: On Win2k and later, the authentication data is now
355 encrypted before saved. The encryption is not available
356 for the other OS's. (Stefan)
357 Index: trunk/src/Resources/TortoiseProcENG.rc
358 ===================================================================
359 diff --git a/trunk/src/Resources/TortoiseProcENG.rc b/trunk/src/Resources/TortoiseProcENG.rc
360 --- a/trunk/src/Resources/TortoiseProcENG.rc (revision 1487)
361 +++ b/trunk/src/Resources/TortoiseProcENG.rc (revision 1488)
362 @@ -398,27 +398,31 @@
363 BEGIN
364 CONTROL "&Indicate folders with changed contents",
365 IDC_CHANGEDDIRS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,
366 - 20,145,10
367 + 20,206,10
368 CONTROL "&Removable drives",IDC_REMOVABLE,"Button",
369 - BS_AUTOCHECKBOX | WS_TABSTOP,18,66,130,10
370 + BS_AUTOCHECKBOX | WS_TABSTOP,18,58,130,10
371 CONTROL "&Network drives",IDC_NETWORK,"Button",BS_AUTOCHECKBOX |
372 - WS_TABSTOP,18,76,130,10
373 + WS_TABSTOP,18,68,130,10
374 CONTROL "&Fixed drives",IDC_FIXED,"Button",BS_AUTOCHECKBOX |
375 - WS_TABSTOP,18,87,127,10
376 + WS_TABSTOP,18,79,127,10
377 CONTROL "&CD-ROM",IDC_CDROM,"Button",BS_AUTOCHECKBOX |
378 - WS_TABSTOP,159,66,118,10
379 - GROUPBOX "Drive Types",IDC_DRIVEGROUP,12,52,274,50
380 + WS_TABSTOP,166,58,118,10
381 + GROUPBOX "Drive Types",IDC_DRIVEGROUP,12,44,274,50
382 CONTROL "RAM drives",IDC_RAM,"Button",BS_AUTOCHECKBOX |
383 - WS_TABSTOP,159,76,119,10
384 + WS_TABSTOP,166,68,119,10
385 CONTROL "Unknown drives",IDC_UNKNOWN,"Button",BS_AUTOCHECKBOX |
386 - WS_TABSTOP,159,86,118,10
387 + WS_TABSTOP,166,78,118,10
388 CONTROL "Show overlays only in explorer",IDC_ONLYEXPLORER,"Button",
389 - BS_AUTOCHECKBOX | WS_TABSTOP,12,33,122,10
390 - GROUPBOX "Icon Overlays / Status Columns",IDC_STATIC,7,7,286,103
391 - GROUPBOX "Context Menu",IDC_STATIC,7,113,286,97
392 + BS_AUTOCHECKBOX | WS_TABSTOP,12,33,190,10
393 + GROUPBOX "Icon Overlays / Status Columns",IDC_STATIC,7,7,286,118
394 + GROUPBOX "Context Menu",IDC_STATIC,7,130,286,80
395 CONTROL "",IDC_MENULIST,"SysListView32",LVS_REPORT |
396 LVS_SINGLESEL | LVS_ALIGNLEFT | LVS_NOCOLUMNHEADER |
397 - WS_BORDER | WS_TABSTOP,12,125,274,78
398 + WS_BORDER | WS_TABSTOP,12,140,274,63
399 + LTEXT "Exclude paths:",IDC_STATIC,12,106,85,8
400 + EDITTEXT IDC_EXCLUDEPATHS,102,96,184,25,ES_MULTILINE |
401 + ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_WANTRETURN |
402 + WS_VSCROLL
403 END
404
405 IDD_SETTINGSPROXY DIALOGEX 0, 0, 300, 217
406 @@ -860,7 +864,7 @@
407 RIGHTMARGIN, 293
408 VERTGUIDE, 12
409 VERTGUIDE, 18
410 - VERTGUIDE, 159
411 + VERTGUIDE, 166
412 VERTGUIDE, 286
413 TOPMARGIN, 7
414 BOTTOMMARGIN, 210
415 @@ -1377,6 +1381,8 @@
416 "If activated, prevents the overlays from showing in ""save as.."" or ""open"" dialogs"
417 IDS_SETTINGS_MENULAYOUT_TT
418 "Check those menu entries you want to appear in the top context menu instead of the submenu"
419 + IDS_SETTINGS_EXCLUDELIST_TT
420 + "A newline separated list of paths for which no icon overlays are shown.\nIf you add an ""*"" char at the end of a path, then all files and subdirs inside that path are excluded too.\nAn empty list will allow overlays on all paths."
421 END
422
423 STRINGTABLE
424 Index: trunk/src/TortoiseProc/SetOverlayPage.cpp
425 ===================================================================
426 diff --git a/trunk/src/TortoiseProc/SetOverlayPage.cpp b/trunk/src/TortoiseProc/SetOverlayPage.cpp
427 --- a/trunk/src/TortoiseProc/SetOverlayPage.cpp (revision 1487)
428 +++ b/trunk/src/TortoiseProc/SetOverlayPage.cpp (revision 1488)
429 @@ -20,6 +20,7 @@
430 #include "TortoiseProc.h"
431 #include "SetOverlayPage.h"
432 #include "Globals.h"
433 +#include ".\setoverlaypage.h"
434
435
436 // CSetOverlayPage dialog
437 @@ -35,6 +36,7 @@
438 , m_bRAM(FALSE)
439 , m_bUnknown(FALSE)
440 , m_bOnlyExplorer(FALSE)
441 + , m_sExcludePaths(_T(""))
442 {
443 m_regShowChangedDirs = CRegDWORD(_T("Software\\TortoiseSVN\\RecursiveOverlay"));
444 m_regOnlyExplorer = CRegDWORD(_T("Software\\TortoiseSVN\\OverlaysOnlyInExplorer"), FALSE);
445 @@ -45,6 +47,7 @@
446 m_regDriveMaskRAM = CRegDWORD(_T("Software\\TortoiseSVN\\DriveMaskRAM"));
447 m_regDriveMaskUnknown = CRegDWORD(_T("Software\\TortoiseSVN\\DriveMaskUnknown"));
448 m_regTopmenu = CRegDWORD(_T("Software\\TortoiseSVN\\ContextMenuEntries"), MENUCHECKOUT | MENUUPDATE | MENUCOMMIT);
449 + m_regExcludePaths = CRegString(_T("Software\\TortoiseSVN\\OverlayExcludeList"));
450
451 m_bShowChangedDirs = m_regShowChangedDirs;
452 m_bOnlyExplorer = m_regOnlyExplorer;
453 @@ -55,6 +58,8 @@
454 m_bRAM = m_regDriveMaskRAM;
455 m_bUnknown = m_regDriveMaskUnknown;
456 m_topmenu = m_regTopmenu;
457 + m_sExcludePaths = m_regExcludePaths;
458 + m_sExcludePaths.Replace(_T("\n"), _T("\r\n"));
459 }
460
461 CSetOverlayPage::~CSetOverlayPage()
462 @@ -74,6 +79,7 @@
463 DDX_Control(pDX, IDC_DRIVEGROUP, m_cDriveGroup);
464 DDX_Check(pDX, IDC_ONLYEXPLORER, m_bOnlyExplorer);
465 DDX_Control(pDX, IDC_MENULIST, m_cMenuList);
466 + DDX_Text(pDX, IDC_EXCLUDEPATHS, m_sExcludePaths);
467 }
468
469
470 @@ -87,6 +93,7 @@
471 ON_BN_CLICKED(IDC_RAM, OnBnClickedRam)
472 ON_BN_CLICKED(IDC_ONLYEXPLORER, OnBnClickedOnlyexplorer)
473 ON_NOTIFY(LVN_ITEMCHANGED, IDC_MENULIST, OnLvnItemchangedMenulist)
474 + ON_EN_CHANGE(IDC_EXCLUDEPATHS, OnEnChangeExcludepaths)
475 END_MESSAGE_MAP()
476
477
478 @@ -103,6 +110,9 @@
479 m_regDriveMaskRAM = m_bRAM;
480 m_regDriveMaskUnknown = m_bUnknown;
481 m_regTopmenu = m_topmenu;
482 + m_sExcludePaths.Replace(_T("\r"), _T(""));
483 + m_regExcludePaths = m_sExcludePaths;
484 + m_sExcludePaths.Replace(_T("\n"), _T("\r\n"));
485 }
486 }
487
488 @@ -116,7 +126,7 @@
489 m_tooltips.AddTool(IDC_CHANGEDDIRS, IDS_SETTINGS_CHANGEDDIRS_TT);
490 m_tooltips.AddTool(IDC_ONLYEXPLORER, IDS_SETTINGS_ONLYEXPLORER_TT);
491 m_tooltips.AddTool(IDC_MENULIST, IDS_SETTINGS_MENULAYOUT_TT);
492 -
493 + m_tooltips.AddTool(IDC_EXCLUDEPATHS, IDS_SETTINGS_EXCLUDELIST_TT);
494
495 m_cMenuList.SetExtendedStyle(LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER);
496
497 @@ -280,3 +290,8 @@
498 } // if (m_cMenuList.GetItemCount() > 0)
499 *pResult = 0;
500 }
501 +
502 +void CSetOverlayPage::OnEnChangeExcludepaths()
503 +{
504 + SetModified();
505 +}
506 Index: trunk/src/TortoiseProc/SetOverlayPage.h
507 ===================================================================
508 diff --git a/trunk/src/TortoiseProc/SetOverlayPage.h b/trunk/src/TortoiseProc/SetOverlayPage.h
509 --- a/trunk/src/TortoiseProc/SetOverlayPage.h (revision 1487)
510 +++ b/trunk/src/TortoiseProc/SetOverlayPage.h (revision 1488)
511 @@ -92,6 +92,8 @@
512 CIconStatic m_cDriveGroup;
513 BOOL m_bInitialized;
514 CRegDWORD m_regTopmenu;
515 + CRegString m_regExcludePaths;
516 + CString m_sExcludePaths;
517
518 CImageList m_imgList;
519 CListCtrl m_cMenuList;
520 @@ -110,4 +112,5 @@
521 virtual BOOL OnApply();
522 afx_msg void OnBnClickedOnlyexplorer();
523 afx_msg void OnLvnItemchangedMenulist(NMHDR *pNMHDR, LRESULT *pResult);
524 + afx_msg void OnEnChangeExcludepaths();
525 };
526 Index: trunk/src/TortoiseProc/resource.h
527 ===================================================================
528 diff --git a/trunk/src/TortoiseProc/resource.h b/trunk/src/TortoiseProc/resource.h
529 --- a/trunk/src/TortoiseProc/resource.h (revision 1487)
530 +++ b/trunk/src/TortoiseProc/resource.h (revision 1488)
531 @@ -179,6 +179,7 @@
532 #define IDC_MINLOGSIZE 1077
533 #define IDC_BUGID 1077
534 #define IDC_WCURL 1077
535 +#define IDC_EXCLUDEPATHS 1077
536 #define IDC_DRIVEGROUP 1079
537 #define IDC_PROXYGROUP 1080
538 #define IDC_SSHGROUP 1081
539 @@ -427,6 +428,7 @@
540 #define IDS_SETTINGS_CHECKNEWER_TT 3100
541 #define IDS_SETTINGS_ONLYEXPLORER_TT 3101
542 #define IDS_SETTINGS_MENULAYOUT_TT 3102
543 +#define IDS_SETTINGS_EXCLUDELIST_TT 3103
544 #define IDS_CHECKNEWER_YOURVERSION 3200
545 #define IDS_CHECKNEWER_CURRENTVERSION 3201
546 #define IDS_CHECKNEWER_YOURUPTODATE 3202
547 Index: trunk/src/TortoiseShell/ShellCache.h
548 ===================================================================
549 diff --git a/trunk/src/TortoiseShell/ShellCache.h b/trunk/src/TortoiseShell/ShellCache.h
550 --- a/trunk/src/TortoiseShell/ShellCache.h (revision 1487)
551 +++ b/trunk/src/TortoiseShell/ShellCache.h (revision 1488)
552 @@ -21,9 +21,11 @@
553 #include "globals.h"
554 #include <tchar.h>
555 #include <string>
556 +#include <vector>
557 #include "registry.h"
558
559 #define REGISTRYTIMEOUT 2000
560 +#define EXCLUDELISTTIMEOUT 5000
561 #define DRIVETYPETIMEOUT 300000 // 5 min
562 #define NUMBERFMTTIMEOUT 300000
563 class ShellCache
564 @@ -39,12 +41,14 @@
565 driveremove = CRegStdWORD(_T("Software\\TortoiseSVN\\DriveMaskRemovable"));
566 driveram = CRegStdWORD(_T("Software\\TortoiseSVN\\DriveMaskRAM"));
567 driveunknown = CRegStdWORD(_T("Software\\TortoiseSVN\\DriveMaskUnknown"));
568 + excludelist = CRegStdString(_T("Software\\TortoiseSVN\\OverlayExcludeList"));
569 recursiveticker = GetTickCount();
570 folderoverlayticker = GetTickCount();
571 driveticker = recursiveticker;
572 drivetypeticker = recursiveticker;
573 langticker = recursiveticker;
574 - columnrevformatticker = langticker;
575 + columnrevformatticker = recursiveticker;
576 + excludelistticker = recursiveticker;
577 menulayout = CRegStdWORD(_T("Software\\TortoiseSVN\\ContextMenuEntries"), MENUCHECKOUT | MENUUPDATE | MENUCOMMIT);
578 langid = CRegStdWORD(_T("Software\\TortoiseSVN\\LanguageID"), 1033);
579 blockstatus = CRegStdWORD(_T("Software\\TortoiseSVN\\BlockStatus"), 0);
580 @@ -177,6 +181,21 @@
581 return FALSE;
582 if ((drivetype == DRIVE_UNKNOWN)&&(IsUnknown()))
583 return FALSE;
584 +
585 + ExcludeListValid();
586 + for (std::vector<stdstring>::iterator I = exvector.begin(); I != exvector.end(); ++I)
587 + {
588 + if (I->empty())
589 + continue;
590 + if (I->at(I->size()-1)=='*')
591 + {
592 + stdstring str = I->substr(0, I->size()-1);
593 + if (_tcsnicmp(str.c_str(), path, str.size())==0)
594 + return FALSE;
595 + }
596 + else if (_tcsicmp(I->c_str(), path)==0)
597 + return FALSE;
598 + }
599 return TRUE;
600 }
601 DWORD GetLangID()
602 @@ -218,6 +237,32 @@
603 driveremove.read();
604 }
605 }
606 + void ExcludeListValid()
607 + {
608 + if ((GetTickCount() - EXCLUDELISTTIMEOUT)>excludelistticker)
609 + {
610 + excludelistticker = GetTickCount();
611 + excludelist.read();
612 + if (excludeliststr.compare((stdstring)excludelist)==0)
613 + return;
614 + excludeliststr = (stdstring)excludelist;
615 + exvector.clear();
616 + int pos = 0, pos_ant = 0;
617 + pos = excludeliststr.find(_T("\n"), pos_ant);
618 + while (pos != stdstring::npos)
619 + {
620 + stdstring token = excludeliststr.substr(pos_ant, pos-pos_ant);
621 + exvector.push_back(token);
622 + pos_ant = pos+1;
623 + pos = excludeliststr.find(_T("\n"), pos_ant);
624 + }
625 + if (!excludeliststr.empty())
626 + {
627 + exvector.push_back(excludeliststr.substr(pos_ant, excludeliststr.size()-1));
628 + }
629 + excludeliststr = (stdstring)excludelist;
630 + }
631 + }
632 CRegStdWORD blockstatus;
633 CRegStdWORD langid;
634 CRegStdWORD showrecursive;
635 @@ -229,6 +274,9 @@
636 CRegStdWORD driveram;
637 CRegStdWORD driveunknown;
638 CRegStdWORD menulayout;
639 + CRegStdString excludelist;
640 + stdstring excludeliststr;
641 + std::vector<stdstring> exvector;
642 DWORD recursiveticker;
643 DWORD folderoverlayticker;
644 DWORD driveticker;
645 @@ -237,6 +285,7 @@
646 DWORD langticker;
647 DWORD blockstatusticker;
648 DWORD columnrevformatticker;
649 + DWORD excludelistticker;
650 UINT drivetypecache[27];
651 TCHAR drivetypepathcache[MAX_PATH];
652 NUMBERFMT columnrevformat; No newline at end of file
@@ -0,0 +1,192 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22
23 import mock
24 import pytest
25
26 from rhodecode.controllers.files import FilesController
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs.backends.base import EmptyCommit
32 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.lib.vcs.nodes import FileNode
34 from rhodecode.model.db import Repository
35 from rhodecode.model.scm import ScmModel
36 from rhodecode.tests import (
37 url, TEST_USER_ADMIN_LOGIN, assert_session_flash, assert_not_in_session_flash)
38 from rhodecode.tests.fixture import Fixture
39 from rhodecode.tests.utils import commit_change
40
41 fixture = Fixture()
42
43
44 @pytest.mark.usefixtures("autologin_user", "app")
45 class TestSideBySideDiff(object):
46
47 def test_diff_side_by_side(self, app, backend, backend_stub):
48 f_path = 'test_sidebyside_file.py'
49 commit1_content = 'content-25d7e49c18b159446c\n'
50 commit2_content = 'content-603d6c72c46d953420\n'
51 repo = backend.create_repo()
52
53 commit1 = commit_change(
54 repo.repo_name, filename=f_path, content=commit1_content,
55 message='A', vcs_type=backend.alias, parent=None, newfile=True)
56
57 commit2 = commit_change(
58 repo.repo_name, filename=f_path, content=commit2_content,
59 message='B, child of A', vcs_type=backend.alias, parent=commit1)
60
61 compare_url = url(
62 'compare_url',
63 repo_name=repo.repo_name,
64 source_ref_type='rev',
65 source_ref=commit1.raw_id,
66 target_repo=repo.repo_name,
67 target_ref_type='rev',
68 target_ref=commit2.raw_id,
69 f_path=f_path,
70 diffmode='sidebyside')
71
72 response = self.app.get(compare_url)
73
74 response.mustcontain('Expand 1 commit')
75 response.mustcontain('1 file changed')
76
77 response.mustcontain(
78 'r%s:%s...r%s:%s' % (
79 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
80
81 response.mustcontain('<strong>{}</strong>'.format(f_path))
82
83 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
84 commits = [
85 {'message': 'First commit'},
86 {'message': 'Commit with binary',
87 'added': [nodes.FileNode('file.empty', content='')]},
88 ]
89 f_path = 'file.empty'
90 repo = backend.create_repo(commits=commits)
91 commit1 = repo.get_commit(commit_idx=0)
92 commit2 = repo.get_commit(commit_idx=1)
93
94 compare_url = url(
95 'compare_url',
96 repo_name=repo.repo_name,
97 source_ref_type='rev',
98 source_ref=commit1.raw_id,
99 target_repo=repo.repo_name,
100 target_ref_type='rev',
101 target_ref=commit2.raw_id,
102 f_path=f_path,
103 diffmode='sidebyside')
104
105 response = self.app.get(compare_url)
106
107 response.mustcontain('Expand 1 commit')
108 response.mustcontain('1 file changed')
109
110 response.mustcontain(
111 'r%s:%s...r%s:%s' % (
112 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
113
114 response.mustcontain('<strong>{}</strong>'.format(f_path))
115
116 def test_diff_sidebyside_two_commits(self, app, backend):
117 commit_id_range = {
118 'hg': {
119 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
120 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
121 'changes': '21 files changed: 943 inserted, 288 deleted'
122 },
123 'git': {
124 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
125 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
126 'changes': '21 files changed: 943 inserted, 288 deleted'
127 },
128
129 'svn': {
130 'commits': ['336',
131 '337'],
132 'changes': '21 files changed: 943 inserted, 288 deleted'
133 },
134 }
135
136 commit_info = commit_id_range[backend.alias]
137 commit2, commit1 = commit_info['commits']
138 file_changes = commit_info['changes']
139
140 compare_url = url(
141 'compare_url',
142 repo_name=backend.repo_name,
143 source_ref_type='rev',
144 source_ref=commit2,
145 target_repo=backend.repo_name,
146 target_ref_type='rev',
147 target_ref=commit1,
148 diffmode='sidebyside')
149 response = self.app.get(compare_url)
150
151 response.mustcontain('Expand 1 commit')
152 response.mustcontain(file_changes)
153
154 def test_diff_sidebyside_two_commits_single_file(self, app, backend):
155 commit_id_range = {
156 'hg': {
157 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
158 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
159 'changes': '1 file changed: 1 inserted, 1 deleted'
160 },
161 'git': {
162 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
163 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
164 'changes': '1 file changed: 1 inserted, 1 deleted'
165 },
166
167 'svn': {
168 'commits': ['336',
169 '337'],
170 'changes': '1 file changed: 1 inserted, 1 deleted'
171 },
172 }
173 f_path = 'docs/conf.py'
174
175 commit_info = commit_id_range[backend.alias]
176 commit2, commit1 = commit_info['commits']
177 file_changes = commit_info['changes']
178
179 compare_url = url(
180 'compare_url',
181 repo_name=backend.repo_name,
182 source_ref_type='rev',
183 source_ref=commit2,
184 target_repo=backend.repo_name,
185 target_ref_type='rev',
186 target_ref=commit1,
187 f_path=f_path,
188 diffmode='sidebyside')
189 response = self.app.get(compare_url)
190
191 response.mustcontain('Expand 1 commit')
192 response.mustcontain(file_changes)
@@ -1066,7 +1066,7 b' def make_map(config):'
1066 '/{repo_name}/annotate/{revision}/{f_path}',
1066 '/{repo_name}/annotate/{revision}/{f_path}',
1067 controller='files', action='index', revision='tip',
1067 controller='files', action='index', revision='tip',
1068 f_path='', annotate=True, conditions={'function': check_repo},
1068 f_path='', annotate=True, conditions={'function': check_repo},
1069 requirements=URL_NAME_REQUIREMENTS)
1069 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1070
1070
1071 rmap.connect('files_edit',
1071 rmap.connect('files_edit',
1072 '/{repo_name}/edit/{revision}/{f_path}',
1072 '/{repo_name}/edit/{revision}/{f_path}',
@@ -90,6 +90,7 b' class CompareController(BaseRepoControll'
90 c.target_ref_type = ""
90 c.target_ref_type = ""
91 c.commit_statuses = ChangesetStatus.STATUSES
91 c.commit_statuses = ChangesetStatus.STATUSES
92 c.preview_mode = False
92 c.preview_mode = False
93 c.file_path = None
93 return render('compare/compare_diff.html')
94 return render('compare/compare_diff.html')
94
95
95 @LoginRequired()
96 @LoginRequired()
@@ -103,8 +104,10 b' class CompareController(BaseRepoControll'
103
104
104 # target_ref will be evaluated in target_repo
105 # target_ref will be evaluated in target_repo
105 target_repo_name = request.GET.get('target_repo', source_repo_name)
106 target_repo_name = request.GET.get('target_repo', source_repo_name)
106 target_path, target_id = parse_path_ref(target_ref)
107 target_path, target_id = parse_path_ref(
108 target_ref, default_path=request.GET.get('f_path', ''))
107
109
110 c.file_path = target_path
108 c.commit_statuses = ChangesetStatus.STATUSES
111 c.commit_statuses = ChangesetStatus.STATUSES
109
112
110 # if merge is True
113 # if merge is True
@@ -115,7 +118,6 b' class CompareController(BaseRepoControll'
115 # if merge is False
118 # if merge is False
116 # Show a raw diff of source/target refs even if no ancestor exists
119 # Show a raw diff of source/target refs even if no ancestor exists
117
120
118
119 # c.fulldiff disables cut_off_limit
121 # c.fulldiff disables cut_off_limit
120 c.fulldiff = str2bool(request.GET.get('fulldiff'))
122 c.fulldiff = str2bool(request.GET.get('fulldiff'))
121
123
@@ -131,7 +133,8 b' class CompareController(BaseRepoControll'
131 target_repo=source_repo_name,
133 target_repo=source_repo_name,
132 target_ref_type=source_ref_type,
134 target_ref_type=source_ref_type,
133 target_ref=source_ref,
135 target_ref=source_ref,
134 merge=merge and '1' or '')
136 merge=merge and '1' or '',
137 f_path=target_path)
135
138
136 source_repo = Repository.get_by_repo_name(source_repo_name)
139 source_repo = Repository.get_by_repo_name(source_repo_name)
137 target_repo = Repository.get_by_repo_name(target_repo_name)
140 target_repo = Repository.get_by_repo_name(target_repo_name)
@@ -151,8 +154,11 b' class CompareController(BaseRepoControll'
151 h.flash(msg, category='error')
154 h.flash(msg, category='error')
152 return redirect(url('compare_home', repo_name=c.repo_name))
155 return redirect(url('compare_home', repo_name=c.repo_name))
153
156
154 source_alias = source_repo.scm_instance().alias
157 source_scm = source_repo.scm_instance()
155 target_alias = target_repo.scm_instance().alias
158 target_scm = target_repo.scm_instance()
159
160 source_alias = source_scm.alias
161 target_alias = target_scm.alias
156 if source_alias != target_alias:
162 if source_alias != target_alias:
157 msg = _('The comparison of two different kinds of remote repos '
163 msg = _('The comparison of two different kinds of remote repos '
158 'is not available')
164 'is not available')
@@ -175,9 +181,6 b' class CompareController(BaseRepoControll'
175 c.source_ref_type = source_ref_type
181 c.source_ref_type = source_ref_type
176 c.target_ref_type = target_ref_type
182 c.target_ref_type = target_ref_type
177
183
178 source_scm = source_repo.scm_instance()
179 target_scm = target_repo.scm_instance()
180
181 pre_load = ["author", "branch", "date", "message"]
184 pre_load = ["author", "branch", "date", "message"]
182 c.ancestor = None
185 c.ancestor = None
183 try:
186 try:
@@ -199,9 +202,9 b' class CompareController(BaseRepoControll'
199 c.statuses = c.rhodecode_db_repo.statuses(
202 c.statuses = c.rhodecode_db_repo.statuses(
200 [x.raw_id for x in c.commit_ranges])
203 [x.raw_id for x in c.commit_ranges])
201
204
202 if partial: # for PR ajax commits loader
205 if partial: # for PR ajax commits loader
203 if not c.ancestor:
206 if not c.ancestor:
204 return '' # cannot merge if there is no ancestor
207 return '' # cannot merge if there is no ancestor
205 return render('compare/compare_commits.html')
208 return render('compare/compare_commits.html')
206
209
207 if c.ancestor:
210 if c.ancestor:
@@ -238,7 +241,8 b' class CompareController(BaseRepoControll'
238
241
239 txtdiff = source_repo.scm_instance().get_diff(
242 txtdiff = source_repo.scm_instance().get_diff(
240 commit1=source_commit, commit2=target_commit,
243 commit1=source_commit, commit2=target_commit,
241 path1=source_path, path=target_path)
244 path=target_path, path1=source_path)
245
242 diff_processor = diffs.DiffProcessor(
246 diff_processor = diffs.DiffProcessor(
243 txtdiff, format='newdiff', diff_limit=diff_limit,
247 txtdiff, format='newdiff', diff_limit=diff_limit,
244 file_limit=file_limit, show_full_diff=c.fulldiff)
248 file_limit=file_limit, show_full_diff=c.fulldiff)
@@ -260,5 +264,7 b' class CompareController(BaseRepoControll'
260 ).render_patchset(_parsed, source_ref, target_ref)
264 ).render_patchset(_parsed, source_ref, target_ref)
261
265
262 c.preview_mode = merge
266 c.preview_mode = merge
267 c.source_commit = source_commit
268 c.target_commit = target_commit
263
269
264 return render('compare/compare_diff.html')
270 return render('compare/compare_diff.html')
@@ -799,21 +799,15 b' class FilesController(BaseRepoController'
799 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
799 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
800 'repository.admin')
800 'repository.admin')
801 def diff(self, repo_name, f_path):
801 def diff(self, repo_name, f_path):
802 ignore_whitespace = request.GET.get('ignorews') == '1'
802
803 line_context = request.GET.get('context', 3)
803 c.action = request.GET.get('diff')
804 diff1 = request.GET.get('diff1', '')
804 diff1 = request.GET.get('diff1', '')
805 diff2 = request.GET.get('diff2', '')
805
806
806 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
807 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
807
808
808 diff2 = request.GET.get('diff2', '')
809 ignore_whitespace = str2bool(request.GET.get('ignorews'))
809 c.action = request.GET.get('diff')
810 line_context = request.GET.get('context', 3)
810 c.no_changes = diff1 == diff2
811 c.f_path = f_path
812 c.big_diff = False
813 c.ignorews_url = _ignorews_url
814 c.context_url = _context_url
815 c.changes = OrderedDict()
816 c.changes[diff2] = []
817
811
818 if not any((diff1, diff2)):
812 if not any((diff1, diff2)):
819 h.flash(
813 h.flash(
@@ -821,18 +815,16 b' class FilesController(BaseRepoController'
821 category='error')
815 category='error')
822 raise HTTPBadRequest()
816 raise HTTPBadRequest()
823
817
824 # special case if we want a show commit_id only, it's impl here
818 if c.action not in ['download', 'raw']:
825 # to reduce JS and callbacks
819 # redirect to new view if we render diff
826
820 return redirect(
827 if request.GET.get('show_rev') and diff1:
821 url('compare_url', repo_name=repo_name,
828 if str2bool(request.GET.get('annotate', 'False')):
822 source_ref_type='rev',
829 _url = url('files_annotate_home', repo_name=c.repo_name,
823 source_ref=diff1,
830 revision=diff1, f_path=path1)
824 target_repo=c.repo_name,
831 else:
825 target_ref_type='rev',
832 _url = url('files_home', repo_name=c.repo_name,
826 target_ref=diff2,
833 revision=diff1, f_path=path1)
827 f_path=f_path))
834
835 return redirect(_url)
836
828
837 try:
829 try:
838 node1 = self._get_file_node(diff1, path1)
830 node1 = self._get_file_node(diff1, path1)
@@ -877,98 +869,40 b' class FilesController(BaseRepoController'
877 return diff.as_raw()
869 return diff.as_raw()
878
870
879 else:
871 else:
880 fid = h.FID(diff2, node2.path)
872 return redirect(
881 line_context_lcl = get_line_ctx(fid, request.GET)
873 url('compare_url', repo_name=repo_name,
882 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
874 source_ref_type='rev',
883
875 source_ref=diff1,
884 __, commit1, commit2, diff, st, data = diffs.wrapped_diff(
876 target_repo=c.repo_name,
885 filenode_old=node1,
877 target_ref_type='rev',
886 filenode_new=node2,
878 target_ref=diff2,
887 diff_limit=self.cut_off_limit_diff,
879 f_path=f_path))
888 file_limit=self.cut_off_limit_file,
889 show_full_diff=request.GET.get('fulldiff'),
890 ignore_whitespace=ign_whitespace_lcl,
891 line_context=line_context_lcl,)
892
893 c.lines_added = data['stats']['added'] if data else 0
894 c.lines_deleted = data['stats']['deleted'] if data else 0
895 c.files = [data]
896 c.commit_ranges = [c.commit_1, c.commit_2]
897 c.ancestor = None
898 c.statuses = []
899 c.target_repo = c.rhodecode_db_repo
900 c.filename1 = node1.path
901 c.filename = node2.path
902 c.binary_file = node1.is_binary or node2.is_binary
903 operation = data['operation'] if data else ''
904
905 commit_changes = {
906 # TODO: it's passing the old file to the diff to keep the
907 # standard but this is not being used for this template,
908 # but might need both files in the future or a more standard
909 # way to work with that
910 'fid': [commit1, commit2, operation,
911 c.filename, diff, st, data]
912 }
913
914 c.changes = commit_changes
915
916 return render('files/file_diff.html')
917
880
918 @LoginRequired()
881 @LoginRequired()
919 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
882 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
920 'repository.admin')
883 'repository.admin')
921 def diff_2way(self, repo_name, f_path):
884 def diff_2way(self, repo_name, f_path):
885 """
886 Kept only to make OLD links work
887 """
922 diff1 = request.GET.get('diff1', '')
888 diff1 = request.GET.get('diff1', '')
923 diff2 = request.GET.get('diff2', '')
889 diff2 = request.GET.get('diff2', '')
924
890
925 nodes = []
891 if not any((diff1, diff2)):
926 unknown_commits = []
892 h.flash(
927 for commit in [diff1, diff2]:
893 'Need query parameter "diff1" or "diff2" to generate a diff.',
928 try:
894 category='error')
929 nodes.append(self._get_file_node(commit, f_path))
895 raise HTTPBadRequest()
930 except (RepositoryError, NodeError):
931 log.exception('%(commit)s does not exist' % {'commit': commit})
932 unknown_commits.append(commit)
933 h.flash(h.literal(
934 _('Commit %(commit)s does not exist.') % {'commit': commit}
935 ), category='error')
936
937 if unknown_commits:
938 return redirect(url('files_home', repo_name=c.repo_name,
939 f_path=f_path))
940
941 if all(isinstance(node.commit, EmptyCommit) for node in nodes):
942 raise HTTPNotFound
943
944 node1, node2 = nodes
945
896
946 f_gitdiff = diffs.get_gitdiff(node1, node2, ignore_whitespace=False)
897 return redirect(
947 diff_processor = diffs.DiffProcessor(f_gitdiff, format='gitdiff')
898 url('compare_url', repo_name=repo_name,
948 diff_data = diff_processor.prepare()
899 source_ref_type='rev',
949
900 source_ref=diff1,
950 if not diff_data or diff_data[0]['raw_diff'] == '':
901 target_repo=c.repo_name,
951 h.flash(h.literal(_('%(file_path)s has not changed '
902 target_ref_type='rev',
952 'between %(commit_1)s and %(commit_2)s.') % {
903 target_ref=diff2,
953 'file_path': f_path,
904 f_path=f_path,
954 'commit_1': node1.commit.id,
905 diffmode='sideside'))
955 'commit_2': node2.commit.id
956 }), category='error')
957 return redirect(url('files_home', repo_name=c.repo_name,
958 f_path=f_path))
959
960 c.diff_data = diff_data[0]
961 c.FID = h.FID(diff2, node2.path)
962 # cleanup some unneeded data
963 del c.diff_data['raw_diff']
964 del c.diff_data['chunks']
965
966 c.node1 = node1
967 c.commit_1 = node1.commit
968 c.node2 = node2
969 c.commit_2 = node2.commit
970
971 return render('files/diff_2way.html')
972
906
973 def _get_file_node(self, commit_id, f_path):
907 def _get_file_node(self, commit_id, f_path):
974 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
908 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
@@ -27,6 +27,7 b' Should only contain utilities to be shar'
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.vcs.exceptions import RepositoryError
28 from rhodecode.lib.vcs.exceptions import RepositoryError
29
29
30
30 def parse_path_ref(ref, default_path=None):
31 def parse_path_ref(ref, default_path=None):
31 """
32 """
32 Parse out a path and reference combination and return both parts of it.
33 Parse out a path and reference combination and return both parts of it.
@@ -76,8 +77,8 b' def get_commit_from_ref_name(repo, ref_n'
76 }
77 }
77
78
78 commit_id = ref_name
79 commit_id = ref_name
79 if repo_scm.alias != 'svn': # pass svn refs straight to backend until
80 if repo_scm.alias != 'svn': # pass svn refs straight to backend until
80 # the branch issue with svn is fixed
81 # the branch issue with svn is fixed
81 if ref_type and ref_type in ref_type_mapping:
82 if ref_type and ref_type in ref_type_mapping:
82 try:
83 try:
83 commit_id = ref_type_mapping[ref_type][ref_name]
84 commit_id = ref_type_mapping[ref_type][ref_name]
@@ -378,6 +378,7 b' class BaseRepository(object):'
378 parameter works only for backends which support diff generation for
378 parameter works only for backends which support diff generation for
379 different paths. Other backends will raise a `ValueError` if `path1`
379 different paths. Other backends will raise a `ValueError` if `path1`
380 is set and has a different value than `path`.
380 is set and has a different value than `path`.
381 :param file_path: filter this diff by given path pattern
381 """
382 """
382 raise NotImplementedError
383 raise NotImplementedError
383
384
@@ -1540,9 +1541,10 b' class Diff(object):'
1540 """
1541 """
1541 Represents a diff result from a repository backend.
1542 Represents a diff result from a repository backend.
1542
1543
1543 Subclasses have to provide a backend specific value for :attr:`_header_re`.
1544 Subclasses have to provide a backend specific value for
1545 :attr:`_header_re` and :attr:`_meta_re`.
1544 """
1546 """
1545
1547 _meta_re = None
1546 _header_re = None
1548 _header_re = None
1547
1549
1548 def __init__(self, raw_diff):
1550 def __init__(self, raw_diff):
@@ -1554,10 +1556,19 b' class Diff(object):'
1554 to make diffs consistent we must prepend with \n, and make sure
1556 to make diffs consistent we must prepend with \n, and make sure
1555 we can detect last chunk as this was also has special rule
1557 we can detect last chunk as this was also has special rule
1556 """
1558 """
1557 chunks = ('\n' + self.raw).split('\ndiff --git')[1:]
1559
1560 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1561 header = diff_parts[0]
1562
1563 if self._meta_re:
1564 match = self._meta_re.match(header)
1565
1566 chunks = diff_parts[1:]
1558 total_chunks = len(chunks)
1567 total_chunks = len(chunks)
1559 return (DiffChunk(chunk, self, cur_chunk == total_chunks)
1568
1560 for cur_chunk, chunk in enumerate(chunks, start=1))
1569 return (
1570 DiffChunk(chunk, self, cur_chunk == total_chunks)
1571 for cur_chunk, chunk in enumerate(chunks, start=1))
1561
1572
1562
1573
1563 class DiffChunk(object):
1574 class DiffChunk(object):
@@ -30,6 +30,10 b' from rhodecode.lib.vcs.backends import b'
30
30
31 class SubversionDiff(base.Diff):
31 class SubversionDiff(base.Diff):
32
32
33 _meta_re = re.compile(r"""
34 (?:^(?P<svn_bin_patch>Cannot[ ]display:[ ]file[ ]marked[ ]as[ ]a[ ]binary[ ]type.)(?:\n|$))?
35 """, re.VERBOSE | re.MULTILINE)
36
33 _header_re = re.compile(r"""
37 _header_re = re.compile(r"""
34 #^diff[ ]--git
38 #^diff[ ]--git
35 [ ]"?a/(?P<a_path>.+?)"?[ ]"?b/(?P<b_path>.+?)"?\n
39 [ ]"?a/(?P<a_path>.+?)"?[ ]"?b/(?P<b_path>.+?)"?\n
@@ -1477,8 +1477,9 b' table.integrations {'
1477 margin-left: 8px;
1477 margin-left: 8px;
1478 }
1478 }
1479
1479
1480 p.ancestor {
1480 div.ancestor {
1481 margin: @padding 0;
1481 margin: @padding 0;
1482 line-height: 3.0em;
1482 }
1483 }
1483
1484
1484 .cs_icon_td input[type="checkbox"] {
1485 .cs_icon_td input[type="checkbox"] {
@@ -72,6 +72,7 b''
72 }
72 }
73 .disabled {
73 .disabled {
74 opacity: .5;
74 opacity: .5;
75 cursor: inherit;
75 }
76 }
76 .help-block {
77 .help-block {
77 color: inherit;
78 color: inherit;
@@ -73,6 +73,16 b' String.prototype.capitalizeFirstLetter ='
73 };
73 };
74
74
75
75
76 String.prototype.truncateAfter = function(chars, suffix) {
77 var suffix = suffix || '';
78 if (this.length > chars) {
79 return this.substr(0, chars) + suffix;
80 } else {
81 return this;
82 }
83 };
84
85
76 /**
86 /**
77 * Splits remainder
87 * Splits remainder
78 *
88 *
@@ -112,7 +112,7 b''
112
112
113 <div class="fieldset">
113 <div class="fieldset">
114 <div class="left-label">
114 <div class="left-label">
115 ${_('Diffs')}:
115 ${_('Diff options')}:
116 </div>
116 </div>
117 <div class="right-content">
117 <div class="right-content">
118 <div class="diff-actions">
118 <div class="diff-actions">
@@ -29,29 +29,84 b''
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32 <div class="summary-header">
32 <div class="summary-header">
33 <div class="title">
33 <div class="title">
34 <div class="title-content">
35 ${self.repo_page_title(c.rhodecode_db_repo)}
34 ${self.repo_page_title(c.rhodecode_db_repo)}
36 </div>
37 </div>
38 <div class="header-buttons">
39 <a href="${h.url('compare_url', repo_name=c.repo_name, source_ref_type='rev', source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'), target_ref_type='rev', target_ref=c.commit_ranges[-1].raw_id)}"
40 class="btn btn-default">
41 ${_('Show combined compare')}
42 </a>
43 </div>
35 </div>
44 </div>
36 </div>
37
38
39 <div class="summary changeset">
40 <div class="summary-detail">
41 <div class="summary-detail-header">
42 <span class="breadcrumbs files_location">
43 <h4>
44 ${_('Commit Range')}
45 <code>
46 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
47 </code>
48 </h4>
49 </span>
50 </div>
51
52 <div class="fieldset">
53 <div class="left-label">
54 ${_('Diff option')}:
55 </div>
56 <div class="right-content">
57 <div class="header-buttons">
58 <a href="${h.url('compare_url', repo_name=c.repo_name, source_ref_type='rev', source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'), target_ref_type='rev', target_ref=c.commit_ranges[-1].raw_id)}">
59 ${_('Show combined compare')}
60 </a>
61 </div>
62 </div>
63 </div>
45
64
46 <div class="summary-detail">
65 <%doc>
47 <div class="title">
66 ##TODO(marcink): implement this and diff menus
48 <h2>
67 <div class="fieldset">
49 ${self.breadcrumbs_links()}
68 <div class="left-label">
50 </h2>
69 ${_('Diff options')}:
70 </div>
71 <div class="right-content">
72 <div class="diff-actions">
73 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
74 ${_('Raw Diff')}
75 </a>
76 |
77 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
78 ${_('Patch Diff')}
79 </a>
80 |
81 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision='?',diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
82 ${_('Download Diff')}
83 </a>
84 </div>
85 </div>
86 </div>
87 </%doc>
88 </div> <!-- end summary-detail -->
89
90 </div> <!-- end summary -->
91
92 <div id="changeset_compare_view_content">
93 <div class="pull-left">
94 <div class="btn-group">
95 <a
96 class="btn"
97 href="#"
98 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
99 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
100 </a>
101 <a
102 class="btn"
103 href="#"
104 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
105 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
106 </a>
107 </div>
51 </div>
108 </div>
52 </div>
109 ## Commit range generated below
53 <div id="changeset_compare_view_content">
54 ##CS
55 <%include file="../compare/compare_commits.html"/>
110 <%include file="../compare/compare_commits.html"/>
56 <div class="cs_files">
111 <div class="cs_files">
57 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
112 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
@@ -65,7 +120,6 b''
65 commit=commit,
120 commit=commit,
66 )}
121 )}
67 %endfor
122 %endfor
68 </table>
69 </div>
123 </div>
70 </div>
124 </div>
71 </%def>
125 </%def>
@@ -52,45 +52,6 b''
52 </div>
52 </div>
53 </%def>
53 </%def>
54
54
55 <%def name="diff_menu(repo_name, f_path, cs1, cs2, change, file=None)">
56 <%
57 onclick_diff2way = ''
58 if (file and file["exceeds_limit"]):
59 onclick_diff2way = '''return confirm('%s');''' % _("Showing a big diff might take some time and resources, continue?")
60 %>
61
62 % if change in ['A', 'M']:
63 <a href="${h.url('files_home',repo_name=repo_name,f_path=f_path,revision=cs2)}"
64 class="tooltip" title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': cs2[:12]})}">
65 ${_('Show File')}
66 </a>
67 % else:
68 <span
69 class="tooltip" title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': cs2[:12]})}">
70 ${_('Show File')}
71 </span>
72 % endif
73 |
74 <a href="${h.url('files_diff_home',repo_name=repo_name,f_path=f_path,diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}"
75 class="tooltip" title="${h.tooltip(_('Show full diff for this file'))}">
76 ${_('Unified Diff')}
77 </a>
78 |
79 <a href="${h.url('files_diff_2way_home',repo_name=repo_name,f_path=f_path,diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}"
80 class="tooltip" title="${h.tooltip(_('Show full side-by-side diff for this file'))}"} onclick="${onclick_diff2way}">
81 ${_('Side-by-side Diff')}
82 </a>
83 |
84 <a href="${h.url('files_diff_home',repo_name=repo_name,f_path=f_path,diff2=cs2,diff1=cs1,diff='raw')}"
85 class="tooltip" title="${h.tooltip(_('Raw diff'))}">
86 ${_('Raw Diff')}
87 </a>
88 |
89 <a href="${h.url('files_diff_home',repo_name=repo_name,f_path=f_path,diff2=cs2,diff1=cs1,diff='download')}"
90 class="tooltip" title="${h.tooltip(_('Download diff'))}">
91 ${_('Download Diff')}
92 </a>
93 </%def>
94
55
95 <%def name="diff_summary_text(changed_files, lines_added, lines_deleted, limited_diff=False)">
56 <%def name="diff_summary_text(changed_files, lines_added, lines_deleted, limited_diff=False)">
96 % if limited_diff:
57 % if limited_diff:
@@ -162,10 +162,11 b' collapse_all = len(diffset.files) > coll'
162
162
163 <div class="filediffs">
163 <div class="filediffs">
164 %for i, filediff in enumerate(diffset.files):
164 %for i, filediff in enumerate(diffset.files):
165 <%
165
166 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
166 <%
167 over_lines_changed_limit = lines_changed > lines_changed_limit
167 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
168 %>
168 over_lines_changed_limit = lines_changed > lines_changed_limit
169 %>
169 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
170 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
170 <div
171 <div
171 class="filediff"
172 class="filediff"
@@ -414,6 +415,7 b' from rhodecode.lib.diffs import NEW_FILE'
414 if line.modified.lineno:
415 if line.modified.lineno:
415 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
416 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
416 %>
417 %>
418
417 <tr class="cb-line">
419 <tr class="cb-line">
418 <td class="cb-data ${action_class(line.original.action)}"
420 <td class="cb-data ${action_class(line.original.action)}"
419 data-line-number="${line.original.lineno}"
421 data-line-number="${line.original.lineno}"
@@ -544,6 +546,7 b' from rhodecode.lib.diffs import NEW_FILE'
544 <div class="diffset-menu clearinner">
546 <div class="diffset-menu clearinner">
545 <div class="pull-right">
547 <div class="pull-right">
546 <div class="btn-group">
548 <div class="btn-group">
549
547 <a
550 <a
548 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
551 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
549 title="${_('View side by side')}"
552 title="${_('View side by side')}"
@@ -557,20 +560,21 b' from rhodecode.lib.diffs import NEW_FILE'
557 </a>
560 </a>
558 </div>
561 </div>
559 </div>
562 </div>
563
560 <div class="pull-left">
564 <div class="pull-left">
561 <div class="btn-group">
565 <div class="btn-group">
562 <a
566 <a
563 class="btn"
567 class="btn"
564 href="#"
568 href="#"
565 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
569 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
566 <a
570 <a
567 class="btn"
571 class="btn"
568 href="#"
572 href="#"
569 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
573 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
570 <a
574 <a
571 class="btn"
575 class="btn"
572 href="#"
576 href="#"
573 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode')}</a>
577 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
574 </div>
578 </div>
575 </div>
579 </div>
576 </div>
580 </div>
@@ -1,20 +1,17 b''
1 ## Changesets table !
1 ## Changesets table !
2 <%namespace name="base" file="/base/base.html"/>
2 <%namespace name="base" file="/base/base.html"/>
3 <div class="container">
4 %if not c.commit_ranges:
5 <p class="empty_data">${_('No Commits')}</p>
6 %else:
7
3
8 %if c.ancestor:
4 %if c.ancestor:
9 <p class="ancestor">${_('Common Ancestor Commit')}:
5 <div class="ancestor">${_('Common Ancestor Commit')}:
10 <a href="${h.url('changeset_home',
6 <a href="${h.url('changeset_home',
11 repo_name=c.repo_name,
7 repo_name=c.repo_name,
12 revision=c.ancestor)}">
8 revision=c.ancestor)}">
13 ${h.short_id(c.ancestor)}
9 ${h.short_id(c.ancestor)}
14 </a>
10 </a>
15 </p>
11 </div>
16 %endif
12 %endif
17
13
14 <div class="container">
18 <input type="hidden" name="__start__" value="revisions:sequence">
15 <input type="hidden" name="__start__" value="revisions:sequence">
19 <table class="rctable compare_view_commits">
16 <table class="rctable compare_view_commits">
20 <tr>
17 <tr>
@@ -66,9 +63,21 b''
66 </td>
63 </td>
67 </tr>
64 </tr>
68 %endfor
65 %endfor
66 <tr class="compare_select_hidden" style="display: none">
67 <td colspan="5">
68 ${ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)}
69 </td>
70 </tr>
71 % if not c.commit_ranges:
72 <tr class="compare_select">
73 <td colspan="5">
74 ${_('No commits in this compare')}
75 </td>
76 </tr>
77 % endif
69 </table>
78 </table>
70 <input type="hidden" name="__end__" value="revisions:sequence">
79 <input type="hidden" name="__end__" value="revisions:sequence">
71 %endif
80
72 </div>
81 </div>
73
82
74 <script>
83 <script>
@@ -76,7 +85,7 b''
76 var target_expand = $(this);
85 var target_expand = $(this);
77 var cid = target_expand.data('commitId');
86 var cid = target_expand.data('commitId');
78
87
79 ## TODO: dan: extract styles into css, and just toggleClass('open') here
88 // ## TODO: dan: extract styles into css, and just toggleClass('open') here
80 if (target_expand.hasClass('open')){
89 if (target_expand.hasClass('open')){
81 $('#c-'+cid).css({
90 $('#c-'+cid).css({
82 'height': '1.5em',
91 'height': '1.5em',
@@ -34,34 +34,132 b''
34 <div class="box">
34 <div class="box">
35 <div class="title">
35 <div class="title">
36 ${self.repo_page_title(c.rhodecode_db_repo)}
36 ${self.repo_page_title(c.rhodecode_db_repo)}
37 <div class="breadcrumbs">
38 ${_('Compare Commits')}
39 </div>
40 </div>
37 </div>
41
38
39 <div class="summary changeset">
40 <div class="summary-detail">
41 <div class="summary-detail-header">
42 <span class="breadcrumbs files_location">
43 <h4>
44 ${_('Compare Commits')}
45 % if c.file_path:
46 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
47 % endif
48
49 % if c.commit_ranges:
50 <code>
51 r${c.source_commit.revision}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.revision}:${h.short_id(c.target_commit.raw_id)}
52 </code>
53 % endif
54 </h4>
55 </span>
56 </div>
57
58 <div class="fieldset">
59 <div class="left-label">
60 ${_('Target')}:
61 </div>
62 <div class="right-content">
63 <div>
64 <div class="code-header" >
65 <div class="compare_header">
66 ## The hidden elements are replaced with a select2 widget
67 ${h.hidden('compare_source')}
68 </div>
69 </div>
70 </div>
71 </div>
72 </div>
73
74 <div class="fieldset">
75 <div class="left-label">
76 ${_('Source')}:
77 </div>
78 <div class="right-content">
79 <div>
80 <div class="code-header" >
81 <div class="compare_header">
82 ## The hidden elements are replaced with a select2 widget
83 ${h.hidden('compare_target')}
84 </div>
85 </div>
86 </div>
87 </div>
88 </div>
89
90 <div class="fieldset">
91 <div class="left-label">
92 ${_('Actions')}:
93 </div>
94 <div class="right-content">
95 <div>
96 <div class="code-header" >
97 <div class="compare_header">
98
99 <div class="compare-buttons">
100 % if c.compare_home:
101 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
102
103 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
104 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
105 <div id="changeset_compare_view_content">
106 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
107 </div>
108
109 % elif c.preview_mode:
110 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
111 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
112 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
113
114 % else:
115 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
116 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
117
118 ## allow comment only if there are commits to comment on
119 % if c.diffset and c.diffset.files and c.commit_ranges:
120 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
121 % else:
122 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
123 % endif
124 % endif
125 </div>
126 </div>
127 </div>
128 </div>
129 </div>
130 </div>
131
132 <%doc>
133 ##TODO(marcink): implement this and diff menus
134 <div class="fieldset">
135 <div class="left-label">
136 ${_('Diff options')}:
137 </div>
138 <div class="right-content">
139 <div class="diff-actions">
140 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
141 ${_('Raw Diff')}
142 </a>
143 |
144 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
145 ${_('Patch Diff')}
146 </a>
147 |
148 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision='?',diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
149 ${_('Download Diff')}
150 </a>
151 </div>
152 </div>
153 </div>
154 </%doc>
155
156 </div> <!-- end summary-detail -->
157
158 </div> <!-- end summary -->
159
160
42 <div class="table">
161 <div class="table">
43 <div id="codeblock" class="diffblock">
44 <div class="code-header" >
45 <div class="compare_header">
46 ## The hidden elements are replaced with a select2 widget
47 <div class="compare-label">${_('Target')}</div>${h.hidden('compare_source')}
48 <div class="compare-label">${_('Source')}</div>${h.hidden('compare_target')}
49
162
50 %if not c.preview_mode:
51 <div class="compare-label"></div>
52 <div class="compare-buttons">
53 %if not c.compare_home:
54 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}"><i class="icon-refresh"></i> ${_('Swap')}</a>
55 %endif
56 <div id="compare_revs" class="btn btn-primary"><i class ="icon-loop"></i> ${_('Compare Commits')}</div>
57 %if c.diffset and c.diffset.files:
58 <div id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</div>
59 %endif
60 </div>
61 %endif
62 </div>
63 </div>
64 </div>
65 ## use JS script to load it quickly before potentially large diffs render long time
163 ## use JS script to load it quickly before potentially large diffs render long time
66 ## this prevents from situation when large diffs block rendering of select2 fields
164 ## this prevents from situation when large diffs block rendering of select2 fields
67 <script type="text/javascript">
165 <script type="text/javascript">
@@ -241,13 +339,26 b''
241
339
242 </div>
340 </div>
243
341
244 %if c.compare_home:
342 %if not c.compare_home:
245 <div id="changeset_compare_view_content">
343 <div id="changeset_compare_view_content">
246 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
344 <div class="pull-left">
247 </div>
345 <div class="btn-group">
248 %else:
346 <a
249 <div id="changeset_compare_view_content">
347 class="btn"
250 ##CS
348 href="#"
349 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
350 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
351 </a>
352 <a
353 class="btn"
354 href="#"
355 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
356 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
357 </a>
358 </div>
359 </div>
360 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
361 ## commit compare generated below
251 <%include file="compare_commits.html"/>
362 <%include file="compare_commits.html"/>
252 ${cbdiffs.render_diffset_menu()}
363 ${cbdiffs.render_diffset_menu()}
253 ${cbdiffs.render_diffset(c.diffset)}
364 ${cbdiffs.render_diffset(c.diffset)}
@@ -9,11 +9,9 b''
9 </%def>
9 </%def>
10
10
11 <%def name="js_extra()">
11 <%def name="js_extra()">
12 <script type="text/javascript" src="${h.asset('js/mergerly.js', ver=c.rhodecode_version_hash)}"></script>
13 </%def>
12 </%def>
14
13
15 <%def name="css_extra()">
14 <%def name="css_extra()">
16 <link rel="stylesheet" type="text/css" href="${h.asset('css/mergerly.css', ver=c.rhodecode_version_hash)}"/>
17 </%def>
15 </%def>
18
16
19
17
@@ -128,7 +128,7 b''
128 // used for history, and switch to
128 // used for history, and switch to
129 var initialCommitData = {
129 var initialCommitData = {
130 id: null,
130 id: null,
131 text: '${_("Switch To Commit")}',
131 text: '${_("Pick Commit")}',
132 type: 'sha',
132 type: 'sha',
133 raw_id: null,
133 raw_id: null,
134 files_url: null
134 files_url: null
@@ -151,9 +151,47 b''
151
151
152 // file history select2
152 // file history select2
153 select2FileHistorySwitcher('#diff1', initialCommitData, state);
153 select2FileHistorySwitcher('#diff1', initialCommitData, state);
154
155 // show at, diff to actions handlers
154 $('#diff1').on('change', function(e) {
156 $('#diff1').on('change', function(e) {
155 $('#diff').removeClass('disabled').removeAttr("disabled");
157 $('#diff_to_commit').removeClass('disabled').removeAttr("disabled");
156 $('#show_rev').removeClass('disabled').removeAttr("disabled");
158 $('#diff_to_commit').val(_gettext('Diff to Commit ') + e.val.truncateAfter(8, '...'));
159
160 $('#show_at_commit').removeClass('disabled').removeAttr("disabled");
161 $('#show_at_commit').val(_gettext('Show at Commit ') + e.val.truncateAfter(8, '...'));
162 });
163
164 $('#diff_to_commit').on('click', function(e) {
165 var diff1 = $('#diff1').val();
166 var diff2 = $('#diff2').val();
167
168 var url_data = {
169 repo_name: templateContext.repo_name,
170 source_ref: diff1,
171 source_ref_type: 'rev',
172 target_ref: diff2,
173 target_ref_type: 'rev',
174 merge: 1,
175 f_path: state.f_path
176 };
177 window.location = pyroutes.url('compare_url', url_data);
178 });
179
180 $('#show_at_commit').on('click', function(e) {
181 var diff1 = $('#diff1').val();
182
183 var annotate = $('#annotate').val();
184 if (annotate === "True") {
185 var url = pyroutes.url('files_annotate_home',
186 {'repo_name': templateContext.repo_name,
187 'revision': diff1, 'f_path': state.f_path});
188 } else {
189 var url = pyroutes.url('files_home',
190 {'repo_name': templateContext.repo_name,
191 'revision': diff1, 'f_path': state.f_path});
192 }
193 window.location = url;
194
157 });
195 });
158
196
159 // show more authors
197 // show more authors
@@ -46,17 +46,29 b''
46 </div>
46 </div>
47
47
48
48
49 <div id="node_history" class="file_diff_buttons collapsable-content" data-toggle="summary-details">
49 <div class="fieldset collapsable-content" data-toggle="summary-details">
50 ${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')}
50 <div class="left-label">
51 ${_('Show/Diff file')}:
52 </div>
53 <div class="right-content">
51 ${h.hidden('diff1')}
54 ${h.hidden('diff1')}
52 ${h.hidden('diff2',c.file_last_commit.raw_id)}
55 ${h.hidden('diff2',c.commit.raw_id)}
56 ${h.hidden('annotate', c.annotate)}
57 </div>
58 </div>
53
59
54 ${h.submit('diff',_('Diff to Commit'),class_="btn disabled",disabled="true")}
60
55 ${h.submit('show_rev',_('Show at Commit'),class_="btn disabled",disabled="true")}
61 <div class="fieldset collapsable-content" data-toggle="summary-details">
56 ${h.hidden('annotate', c.annotate)}
62 <div class="left-label">
57 ${h.end_form()}
63 ${_('Action')}:
64 </div>
65 <div class="right-content">
66 ${h.submit('diff_to_commit',_('Diff to Commit'),class_="btn disabled",disabled="true")}
67 ${h.submit('show_at_commit',_('Show at Commit'),class_="btn disabled",disabled="true")}
68 </div>
58 </div>
69 </div>
59
70
71
60 <script>
72 <script>
61 collapsableContent();
73 collapsableContent();
62 </script> No newline at end of file
74 </script>
@@ -360,14 +360,33 b''
360 </div>
360 </div>
361 % endif
361 % endif
362 <div class="compare_view_commits_title">
362 <div class="compare_view_commits_title">
363 % if c.allowed_to_update and not c.pull_request.is_closed():
363
364 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
364 <div class="pull-left">
365 % else:
365 <div class="btn-group">
366 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
366 <a
367 % endif
367 class="btn"
368 % if len(c.commit_ranges):
368 href="#"
369 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
369 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
370 % endif
370 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
371 </a>
372 <a
373 class="btn"
374 href="#"
375 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
376 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
377 </a>
378 </div>
379 </div>
380
381 <div class="pull-right">
382 % if c.allowed_to_update and not c.pull_request.is_closed():
383 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
384 % else:
385 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
386 % endif
387
388 </div>
389
371 </div>
390 </div>
372 % if not c.missing_commits:
391 % if not c.missing_commits:
373 <%include file="/compare/compare_commits.html" />
392 <%include file="/compare/compare_commits.html" />
@@ -244,7 +244,8 b' class TestCommitCommentsController(TestC'
244 ('markdown', '# header', '<h1>header</h1>'),
244 ('markdown', '# header', '<h1>header</h1>'),
245 ('markdown', '*italics*', '<em>italics</em>'),
245 ('markdown', '*italics*', '<em>italics</em>'),
246 ('markdown', '**bold**', '<strong>bold</strong>'),
246 ('markdown', '**bold**', '<strong>bold</strong>'),
247 ])
247 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
248 'md-header', 'md-italics', 'md-bold', ])
248 def test_preview(self, renderer, input, output, backend):
249 def test_preview(self, renderer, input, output, backend):
249 self.log_user()
250 self.log_user()
250 params = {
251 params = {
@@ -22,16 +22,13 b' import mock'
22 import pytest
22 import pytest
23 import lxml.html
23 import lxml.html
24
24
25 from rhodecode.lib.vcs.backends.base import EmptyCommit
26 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
27 from rhodecode.model.db import Repository
26 from rhodecode.tests import url, assert_session_flash
28 from rhodecode.model.scm import ScmModel
27 from rhodecode.tests.utils import AssertResponse, commit_change
29 from rhodecode.tests import url, TEST_USER_ADMIN_LOGIN, assert_session_flash
30 from rhodecode.tests.utils import AssertResponse
31
28
32
29
33 @pytest.mark.usefixtures("autologin_user", "app")
30 @pytest.mark.usefixtures("autologin_user", "app")
34 class TestCompareController:
31 class TestCompareController(object):
35
32
36 @pytest.mark.xfail_backends("svn", reason="Requires pull")
33 @pytest.mark.xfail_backends("svn", reason="Requires pull")
37 def test_compare_remote_with_different_commit_indexes(self, backend):
34 def test_compare_remote_with_different_commit_indexes(self, backend):
@@ -53,23 +50,23 b' class TestCompareController:'
53 fork = backend.create_repo()
50 fork = backend.create_repo()
54
51
55 # prepare fork
52 # prepare fork
56 commit0 = _commit_change(
53 commit0 = commit_change(
57 fork.repo_name, filename='file1', content='A',
54 fork.repo_name, filename='file1', content='A',
58 message='A', vcs_type=backend.alias, parent=None, newfile=True)
55 message='A', vcs_type=backend.alias, parent=None, newfile=True)
59
56
60 commit1 = _commit_change(
57 commit1 = commit_change(
61 fork.repo_name, filename='file1', content='B',
58 fork.repo_name, filename='file1', content='B',
62 message='B, child of A', vcs_type=backend.alias, parent=commit0)
59 message='B, child of A', vcs_type=backend.alias, parent=commit0)
63
60
64 _commit_change( # commit 2
61 commit_change( # commit 2
65 fork.repo_name, filename='file1', content='C',
62 fork.repo_name, filename='file1', content='C',
66 message='C, child of B', vcs_type=backend.alias, parent=commit1)
63 message='C, child of B', vcs_type=backend.alias, parent=commit1)
67
64
68 commit3 = _commit_change(
65 commit3 = commit_change(
69 fork.repo_name, filename='file1', content='D',
66 fork.repo_name, filename='file1', content='D',
70 message='D, child of A', vcs_type=backend.alias, parent=commit0)
67 message='D, child of A', vcs_type=backend.alias, parent=commit0)
71
68
72 commit4 = _commit_change(
69 commit4 = commit_change(
73 fork.repo_name, filename='file1', content='E',
70 fork.repo_name, filename='file1', content='E',
74 message='E, child of D', vcs_type=backend.alias, parent=commit3)
71 message='E, child of D', vcs_type=backend.alias, parent=commit3)
75
72
@@ -105,7 +102,7 b' class TestCompareController:'
105 repo1 = backend.create_repo()
102 repo1 = backend.create_repo()
106
103
107 # commit something !
104 # commit something !
108 commit0 = _commit_change(
105 commit0 = commit_change(
109 repo1.repo_name, filename='file1', content='line1\n',
106 repo1.repo_name, filename='file1', content='line1\n',
110 message='commit1', vcs_type=backend.alias, parent=None,
107 message='commit1', vcs_type=backend.alias, parent=None,
111 newfile=True)
108 newfile=True)
@@ -114,11 +111,11 b' class TestCompareController:'
114 repo2 = backend.create_fork()
111 repo2 = backend.create_fork()
115
112
116 # add two extra commit into fork
113 # add two extra commit into fork
117 commit1 = _commit_change(
114 commit1 = commit_change(
118 repo2.repo_name, filename='file1', content='line1\nline2\n',
115 repo2.repo_name, filename='file1', content='line1\nline2\n',
119 message='commit2', vcs_type=backend.alias, parent=commit0)
116 message='commit2', vcs_type=backend.alias, parent=commit0)
120
117
121 commit2 = _commit_change(
118 commit2 = commit_change(
122 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
119 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
123 message='commit3', vcs_type=backend.alias, parent=commit1)
120 message='commit3', vcs_type=backend.alias, parent=commit1)
124
121
@@ -156,7 +153,7 b' class TestCompareController:'
156 repo1 = backend.create_repo()
153 repo1 = backend.create_repo()
157
154
158 # commit something !
155 # commit something !
159 commit0 = _commit_change(
156 commit0 = commit_change(
160 repo1.repo_name, filename='file1', content='line1\n',
157 repo1.repo_name, filename='file1', content='line1\n',
161 message='commit1', vcs_type=backend.alias, parent=None,
158 message='commit1', vcs_type=backend.alias, parent=None,
162 newfile=True)
159 newfile=True)
@@ -165,17 +162,17 b' class TestCompareController:'
165 repo2 = backend.create_fork()
162 repo2 = backend.create_fork()
166
163
167 # now commit something to origin repo
164 # now commit something to origin repo
168 _commit_change(
165 commit_change(
169 repo1.repo_name, filename='file2', content='line1file2\n',
166 repo1.repo_name, filename='file2', content='line1file2\n',
170 message='commit2', vcs_type=backend.alias, parent=commit0,
167 message='commit2', vcs_type=backend.alias, parent=commit0,
171 newfile=True)
168 newfile=True)
172
169
173 # add two extra commit into fork
170 # add two extra commit into fork
174 commit1 = _commit_change(
171 commit1 = commit_change(
175 repo2.repo_name, filename='file1', content='line1\nline2\n',
172 repo2.repo_name, filename='file1', content='line1\nline2\n',
176 message='commit2', vcs_type=backend.alias, parent=commit0)
173 message='commit2', vcs_type=backend.alias, parent=commit0)
177
174
178 commit2 = _commit_change(
175 commit2 = commit_change(
179 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
176 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
180 message='commit3', vcs_type=backend.alias, parent=commit1)
177 message='commit3', vcs_type=backend.alias, parent=commit1)
181
178
@@ -207,9 +204,9 b' class TestCompareController:'
207 compare_page.swap_is_hidden()
204 compare_page.swap_is_hidden()
208 compare_page.target_source_are_disabled()
205 compare_page.target_source_are_disabled()
209
206
210 @pytest.mark.xfail_backends("svn", "git")
207 @pytest.mark.xfail_backends("svn")
208 # TODO(marcink): no svn support for compare two seperate repos
211 def test_compare_of_unrelated_forks(self, backend):
209 def test_compare_of_unrelated_forks(self, backend):
212 # TODO: johbo: Fails for git due to some other issue it seems
213 orig = backend.create_repo(number_of_commits=1)
210 orig = backend.create_repo(number_of_commits=1)
214 fork = backend.create_repo(number_of_commits=1)
211 fork = backend.create_repo(number_of_commits=1)
215
212
@@ -245,11 +242,11 b' class TestCompareController:'
245 repo1 = backend.create_repo()
242 repo1 = backend.create_repo()
246
243
247 # commit something !
244 # commit something !
248 commit0 = _commit_change(
245 commit0 = commit_change(
249 repo1.repo_name, filename='file1', content='line1\n',
246 repo1.repo_name, filename='file1', content='line1\n',
250 message='commit1', vcs_type=backend.alias, parent=None,
247 message='commit1', vcs_type=backend.alias, parent=None,
251 newfile=True)
248 newfile=True)
252 commit1 = _commit_change(
249 commit1 = commit_change(
253 repo1.repo_name, filename='file1', content='line1\nline2\n',
250 repo1.repo_name, filename='file1', content='line1\nline2\n',
254 message='commit2', vcs_type=backend.alias, parent=commit0)
251 message='commit2', vcs_type=backend.alias, parent=commit0)
255
252
@@ -257,18 +254,18 b' class TestCompareController:'
257 repo2 = backend.create_fork()
254 repo2 = backend.create_fork()
258
255
259 # now make commit3-6
256 # now make commit3-6
260 commit2 = _commit_change(
257 commit2 = commit_change(
261 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
258 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
262 message='commit3', vcs_type=backend.alias, parent=commit1)
259 message='commit3', vcs_type=backend.alias, parent=commit1)
263 commit3 = _commit_change(
260 commit3 = commit_change(
264 repo1.repo_name, filename='file1',
261 repo1.repo_name, filename='file1',
265 content='line1\nline2\nline3\nline4\n', message='commit4',
262 content='line1\nline2\nline3\nline4\n', message='commit4',
266 vcs_type=backend.alias, parent=commit2)
263 vcs_type=backend.alias, parent=commit2)
267 commit4 = _commit_change(
264 commit4 = commit_change(
268 repo1.repo_name, filename='file1',
265 repo1.repo_name, filename='file1',
269 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
266 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
270 vcs_type=backend.alias, parent=commit3)
267 vcs_type=backend.alias, parent=commit3)
271 _commit_change( # commit 5
268 commit_change( # commit 5
272 repo1.repo_name, filename='file1',
269 repo1.repo_name, filename='file1',
273 content='line1\nline2\nline3\nline4\nline5\nline6\n',
270 content='line1\nline2\nline3\nline4\nline5\nline6\n',
274 message='commit6', vcs_type=backend.alias, parent=commit4)
271 message='commit6', vcs_type=backend.alias, parent=commit4)
@@ -311,11 +308,11 b' class TestCompareController:'
311 repo1 = backend.create_repo()
308 repo1 = backend.create_repo()
312
309
313 # commit something !
310 # commit something !
314 commit0 = _commit_change(
311 commit0 = commit_change(
315 repo1.repo_name, filename='file1', content='line1\n',
312 repo1.repo_name, filename='file1', content='line1\n',
316 message='commit1', vcs_type=backend.alias, parent=None,
313 message='commit1', vcs_type=backend.alias, parent=None,
317 newfile=True)
314 newfile=True)
318 commit1 = _commit_change(
315 commit1 = commit_change(
319 repo1.repo_name, filename='file1', content='line1\nline2\n',
316 repo1.repo_name, filename='file1', content='line1\nline2\n',
320 message='commit2', vcs_type=backend.alias, parent=commit0)
317 message='commit2', vcs_type=backend.alias, parent=commit0)
321
318
@@ -323,18 +320,18 b' class TestCompareController:'
323 backend.create_fork()
320 backend.create_fork()
324
321
325 # now make commit3-6
322 # now make commit3-6
326 commit2 = _commit_change(
323 commit2 = commit_change(
327 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
324 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
328 message='commit3', vcs_type=backend.alias, parent=commit1)
325 message='commit3', vcs_type=backend.alias, parent=commit1)
329 commit3 = _commit_change(
326 commit3 = commit_change(
330 repo1.repo_name, filename='file1',
327 repo1.repo_name, filename='file1',
331 content='line1\nline2\nline3\nline4\n', message='commit4',
328 content='line1\nline2\nline3\nline4\n', message='commit4',
332 vcs_type=backend.alias, parent=commit2)
329 vcs_type=backend.alias, parent=commit2)
333 commit4 = _commit_change(
330 commit4 = commit_change(
334 repo1.repo_name, filename='file1',
331 repo1.repo_name, filename='file1',
335 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
332 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
336 vcs_type=backend.alias, parent=commit3)
333 vcs_type=backend.alias, parent=commit3)
337 commit5 = _commit_change(
334 commit5 = commit_change(
338 repo1.repo_name, filename='file1',
335 repo1.repo_name, filename='file1',
339 content='line1\nline2\nline3\nline4\nline5\nline6\n',
336 content='line1\nline2\nline3\nline4\nline5\nline6\n',
340 message='commit6', vcs_type=backend.alias, parent=commit4)
337 message='commit6', vcs_type=backend.alias, parent=commit4)
@@ -400,7 +397,7 b' class TestCompareController:'
400 repo1 = backend.create_repo()
397 repo1 = backend.create_repo()
401 r1_name = repo1.repo_name
398 r1_name = repo1.repo_name
402
399
403 commit0 = _commit_change(
400 commit0 = commit_change(
404 repo=r1_name, filename='file1',
401 repo=r1_name, filename='file1',
405 content='line1', message='commit1', vcs_type=backend.alias,
402 content='line1', message='commit1', vcs_type=backend.alias,
406 newfile=True)
403 newfile=True)
@@ -413,19 +410,19 b' class TestCompareController:'
413 self.r2_id = repo2.repo_id
410 self.r2_id = repo2.repo_id
414 r2_name = repo2.repo_name
411 r2_name = repo2.repo_name
415
412
416 commit1 = _commit_change(
413 commit1 = commit_change(
417 repo=r2_name, filename='file1-fork',
414 repo=r2_name, filename='file1-fork',
418 content='file1-line1-from-fork', message='commit1-fork',
415 content='file1-line1-from-fork', message='commit1-fork',
419 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
416 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
420 newfile=True)
417 newfile=True)
421
418
422 commit2 = _commit_change(
419 commit2 = commit_change(
423 repo=r2_name, filename='file2-fork',
420 repo=r2_name, filename='file2-fork',
424 content='file2-line1-from-fork', message='commit2-fork',
421 content='file2-line1-from-fork', message='commit2-fork',
425 vcs_type=backend.alias, parent=commit1,
422 vcs_type=backend.alias, parent=commit1,
426 newfile=True)
423 newfile=True)
427
424
428 _commit_change( # commit 3
425 commit_change( # commit 3
429 repo=r2_name, filename='file3-fork',
426 repo=r2_name, filename='file3-fork',
430 content='file3-line1-from-fork', message='commit3-fork',
427 content='file3-line1-from-fork', message='commit3-fork',
431 vcs_type=backend.alias, parent=commit2, newfile=True)
428 vcs_type=backend.alias, parent=commit2, newfile=True)
@@ -447,9 +444,9 b' class TestCompareController:'
447 response.mustcontain('%s@%s' % (r2_name, commit_id1))
444 response.mustcontain('%s@%s' % (r2_name, commit_id1))
448 response.mustcontain('%s@%s' % (r1_name, commit_id2))
445 response.mustcontain('%s@%s' % (r1_name, commit_id2))
449 response.mustcontain('No files')
446 response.mustcontain('No files')
450 response.mustcontain('No Commits')
447 response.mustcontain('No commits in this compare')
451
448
452 commit0 = _commit_change(
449 commit0 = commit_change(
453 repo=r1_name, filename='file2',
450 repo=r1_name, filename='file2',
454 content='line1-added-after-fork', message='commit2-parent',
451 content='line1-added-after-fork', message='commit2-parent',
455 vcs_type=backend.alias, parent=None, newfile=True)
452 vcs_type=backend.alias, parent=None, newfile=True)
@@ -558,7 +555,7 b' class TestCompareController:'
558
555
559
556
560 @pytest.mark.usefixtures("autologin_user")
557 @pytest.mark.usefixtures("autologin_user")
561 class TestCompareControllerSvn:
558 class TestCompareControllerSvn(object):
562
559
563 def test_supports_references_with_path(self, app, backend_svn):
560 def test_supports_references_with_path(self, app, backend_svn):
564 repo = backend_svn['svn-simple-layout']
561 repo = backend_svn['svn-simple-layout']
@@ -574,7 +571,7 b' class TestCompareControllerSvn:'
574 status=200)
571 status=200)
575
572
576 # Expecting no commits, since both paths are at the same revision
573 # Expecting no commits, since both paths are at the same revision
577 response.mustcontain('No Commits')
574 response.mustcontain('No commits in this compare')
578
575
579 # Should find only one file changed when comparing those two tags
576 # Should find only one file changed when comparing those two tags
580 response.mustcontain('example.py')
577 response.mustcontain('example.py')
@@ -596,7 +593,7 b' class TestCompareControllerSvn:'
596 status=200)
593 status=200)
597
594
598 # It should show commits
595 # It should show commits
599 assert 'No Commits' not in response.body
596 assert 'No commits in this compare' not in response.body
600
597
601 # Should find only one file changed when comparing those two tags
598 # Should find only one file changed when comparing those two tags
602 response.mustcontain('example.py')
599 response.mustcontain('example.py')
@@ -660,36 +657,3 b' class ComparePage(AssertResponse):'
660 def target_source_are_enabled(self):
657 def target_source_are_enabled(self):
661 response = self.response
658 response = self.response
662 response.mustcontain("var enable_fields = true;")
659 response.mustcontain("var enable_fields = true;")
663
664
665 def _commit_change(
666 repo, filename, content, message, vcs_type, parent=None,
667 newfile=False):
668 repo = Repository.get_by_repo_name(repo)
669 _commit = parent
670 if not parent:
671 _commit = EmptyCommit(alias=vcs_type)
672
673 if newfile:
674 nodes = {
675 filename: {
676 'content': content
677 }
678 }
679 commit = ScmModel().create_nodes(
680 user=TEST_USER_ADMIN_LOGIN, repo=repo,
681 message=message,
682 nodes=nodes,
683 parent_commit=_commit,
684 author=TEST_USER_ADMIN_LOGIN,
685 )
686 else:
687 commit = ScmModel().commit_change(
688 repo=repo.scm_instance(), repo_name=repo.repo_name,
689 commit=parent, user=TEST_USER_ADMIN_LOGIN,
690 author=TEST_USER_ADMIN_LOGIN,
691 message=message,
692 content=content,
693 f_path=filename
694 )
695 return commit
@@ -44,7 +44,7 b' class TestCompareController:'
44 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
44 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
45 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
45 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
46
46
47 # outgoing changesets between tags
47 # outgoing commits between tags
48 commit_indexes = {
48 commit_indexes = {
49 'git': [113] + range(115, 121),
49 'git': [113] + range(115, 121),
50 'hg': [112] + range(115, 121),
50 'hg': [112] + range(115, 121),
@@ -118,8 +118,8 b' class TestCompareController:'
118 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
118 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
119
119
120 # branches are equal
120 # branches are equal
121 response.mustcontain('<p class="empty_data">No files</p>')
121 response.mustcontain('No files')
122 response.mustcontain('<p class="empty_data">No Commits</p>')
122 response.mustcontain('No commits in this compare')
123
123
124 def test_compare_commits(self, backend):
124 def test_compare_commits(self, backend):
125 repo = backend.repo
125 repo = backend.repo
@@ -28,15 +28,11 b' from rhodecode.lib import helpers as h'
28 from rhodecode.lib.compat import OrderedDict
28 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.ext_json import json
29 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.vcs import nodes
30 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs.backends.base import EmptyCommit
31
32 from rhodecode.lib.vcs.conf import settings
32 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.lib.vcs.nodes import FileNode
34 from rhodecode.model.db import Repository
35 from rhodecode.model.scm import ScmModel
36 from rhodecode.tests import (
33 from rhodecode.tests import (
37 url, TEST_USER_ADMIN_LOGIN, assert_session_flash, assert_not_in_session_flash)
34 url, assert_session_flash, assert_not_in_session_flash)
38 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.fixture import Fixture
39 from rhodecode.tests.utils import AssertResponse
40
36
41 fixture = Fixture()
37 fixture = Fixture()
42
38
@@ -48,40 +44,6 b' NODE_HISTORY = {'
48
44
49
45
50
46
51 def _commit_change(
52 repo, filename, content, message, vcs_type, parent=None,
53 newfile=False):
54 repo = Repository.get_by_repo_name(repo)
55 _commit = parent
56 if not parent:
57 _commit = EmptyCommit(alias=vcs_type)
58
59 if newfile:
60 nodes = {
61 filename: {
62 'content': content
63 }
64 }
65 commit = ScmModel().create_nodes(
66 user=TEST_USER_ADMIN_LOGIN, repo=repo,
67 message=message,
68 nodes=nodes,
69 parent_commit=_commit,
70 author=TEST_USER_ADMIN_LOGIN,
71 )
72 else:
73 commit = ScmModel().commit_change(
74 repo=repo.scm_instance(), repo_name=repo.repo_name,
75 commit=parent, user=TEST_USER_ADMIN_LOGIN,
76 author=TEST_USER_ADMIN_LOGIN,
77 message=message,
78 content=content,
79 f_path=filename
80 )
81 return commit
82
83
84
85 @pytest.mark.usefixtures("app")
47 @pytest.mark.usefixtures("app")
86 class TestFilesController:
48 class TestFilesController:
87
49
@@ -120,7 +82,7 b' class TestFilesController:'
120 response = self.app.get(url(
82 response = self.app.get(url(
121 controller='files', action='index',
83 controller='files', action='index',
122 repo_name=repo.repo_name, revision='tip', f_path='/'))
84 repo_name=repo.repo_name, revision='tip', f_path='/'))
123 assert_response = AssertResponse(response)
85 assert_response = response.assert_response()
124 assert_response.contains_one_link(
86 assert_response.contains_one_link(
125 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
87 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
126
88
@@ -130,7 +92,7 b' class TestFilesController:'
130 response = self.app.get(url(
92 response = self.app.get(url(
131 controller='files', action='index',
93 controller='files', action='index',
132 repo_name=repo.repo_name, revision='tip', f_path='/'))
94 repo_name=repo.repo_name, revision='tip', f_path='/'))
133 assert_response = AssertResponse(response)
95 assert_response = response.assert_response()
134 assert_response.contains_one_link(
96 assert_response.contains_one_link(
135 'subpaths-path @ 000000000000',
97 'subpaths-path @ 000000000000',
136 'http://sub-base.example.com/subpaths-path')
98 'http://sub-base.example.com/subpaths-path')
@@ -179,21 +141,24 b' class TestFilesController:'
179 assert_dirs_in_response(response, dirs, params)
141 assert_dirs_in_response(response, dirs, params)
180 assert_files_in_response(response, files, params)
142 assert_files_in_response(response, files, params)
181
143
182 @pytest.mark.xfail_backends("git", reason="Missing branches in git repo")
183 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
184 def test_index_different_branch(self, backend):
144 def test_index_different_branch(self, backend):
185 # TODO: Git test repository does not contain branches
145 branches = dict(
186 # TODO: Branch support in Subversion
146 hg=(150, ['git']),
187
147 # TODO: Git test repository does not contain other branches
188 commit = backend.repo.get_commit(commit_idx=150)
148 git=(633, ['master']),
149 # TODO: Branch support in Subversion
150 svn=(150, [])
151 )
152 idx, branches = branches[backend.alias]
153 commit = backend.repo.get_commit(commit_idx=idx)
189 response = self.app.get(url(
154 response = self.app.get(url(
190 controller='files', action='index',
155 controller='files', action='index',
191 repo_name=backend.repo_name,
156 repo_name=backend.repo_name,
192 revision=commit.raw_id,
157 revision=commit.raw_id,
193 f_path='/'))
158 f_path='/'))
194 assert_response = AssertResponse(response)
159 assert_response = response.assert_response()
195 assert_response.element_contains(
160 for branch in branches:
196 '.tags .branchtag', 'git')
161 assert_response.element_contains('.tags .branchtag', branch)
197
162
198 def test_index_paging(self, backend):
163 def test_index_paging(self, backend):
199 repo = backend.repo
164 repo = backend.repo
@@ -221,7 +186,7 b' class TestFilesController:'
221 msgbox = """<div class="commit right-content">%s</div>"""
186 msgbox = """<div class="commit right-content">%s</div>"""
222 response.mustcontain(msgbox % (commit.message, ))
187 response.mustcontain(msgbox % (commit.message, ))
223
188
224 assert_response = AssertResponse(response)
189 assert_response = response.assert_response()
225 if commit.branch:
190 if commit.branch:
226 assert_response.element_contains('.tags.tags-main .branchtag', commit.branch)
191 assert_response.element_contains('.tags.tags-main .branchtag', commit.branch)
227 if commit.tags:
192 if commit.tags:
@@ -348,7 +313,7 b' class TestFilesController:'
348 f_path='/', commit_id=commit.raw_id),
313 f_path='/', commit_id=commit.raw_id),
349 extra_environ=xhr_header)
314 extra_environ=xhr_header)
350
315
351 assert_response = AssertResponse(response)
316 assert_response = response.assert_response()
352
317
353 for attr in ['data-commit-id', 'data-date', 'data-author']:
318 for attr in ['data-commit-id', 'data-date', 'data-author']:
354 elements = assert_response.get_elements('[{}]'.format(attr))
319 elements = assert_response.get_elements('[{}]'.format(attr))
@@ -401,7 +366,7 b' class TestFilesController:'
401 # TODO: johbo: Think about a better place for these tests. Either controller
366 # TODO: johbo: Think about a better place for these tests. Either controller
402 # specific unit tests or we move down the whole logic further towards the vcs
367 # specific unit tests or we move down the whole logic further towards the vcs
403 # layer
368 # layer
404 class TestAdjustFilePathForSvn:
369 class TestAdjustFilePathForSvn(object):
405 """SVN specific adjustments of node history in FileController."""
370 """SVN specific adjustments of node history in FileController."""
406
371
407 def test_returns_path_relative_to_matched_reference(self):
372 def test_returns_path_relative_to_matched_reference(self):
@@ -433,7 +398,7 b' class TestAdjustFilePathForSvn:'
433
398
434
399
435 @pytest.mark.usefixtures("app")
400 @pytest.mark.usefixtures("app")
436 class TestRepositoryArchival:
401 class TestRepositoryArchival(object):
437
402
438 def test_archival(self, backend):
403 def test_archival(self, backend):
439 backend.enable_downloads()
404 backend.enable_downloads()
@@ -485,7 +450,7 b' class TestRepositoryArchival:'
485
450
486
451
487 @pytest.mark.usefixtures("app", "autologin_user")
452 @pytest.mark.usefixtures("app", "autologin_user")
488 class TestRawFileHandling:
453 class TestRawFileHandling(object):
489
454
490 def test_raw_file_ok(self, backend):
455 def test_raw_file_ok(self, backend):
491 commit = backend.repo.get_commit(commit_idx=173)
456 commit = backend.repo.get_commit(commit_idx=173)
@@ -575,6 +540,7 b' class TestFilesDiff:'
575 def test_file_full_diff(self, backend, diff):
540 def test_file_full_diff(self, backend, diff):
576 commit1 = backend.repo.get_commit(commit_idx=-1)
541 commit1 = backend.repo.get_commit(commit_idx=-1)
577 commit2 = backend.repo.get_commit(commit_idx=-2)
542 commit2 = backend.repo.get_commit(commit_idx=-2)
543
578 response = self.app.get(
544 response = self.app.get(
579 url(
545 url(
580 controller='files',
546 controller='files',
@@ -582,11 +548,17 b' class TestFilesDiff:'
582 repo_name=backend.repo_name,
548 repo_name=backend.repo_name,
583 f_path='README'),
549 f_path='README'),
584 params={
550 params={
585 'diff1': commit1.raw_id,
551 'diff1': commit2.raw_id,
586 'diff2': commit2.raw_id,
552 'diff2': commit1.raw_id,
587 'fulldiff': '1',
553 'fulldiff': '1',
588 'diff': diff,
554 'diff': diff,
589 })
555 })
556
557 if diff == 'diff':
558 # use redirect since this is OLD view redirecting to compare page
559 response = response.follow()
560
561 # It's a symlink to README.rst
590 response.mustcontain('README.rst')
562 response.mustcontain('README.rst')
591 response.mustcontain('No newline at end of file')
563 response.mustcontain('No newline at end of file')
592
564
@@ -610,7 +582,17 b' class TestFilesDiff:'
610 'fulldiff': '1',
582 'fulldiff': '1',
611 'diff': 'diff',
583 'diff': 'diff',
612 })
584 })
613 response.mustcontain('Cannot diff binary files')
585 # use redirect since this is OLD view redirecting to compare page
586 response = response.follow()
587 response.mustcontain('Expand 1 commit')
588 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
589
590 if backend.alias == 'svn':
591 response.mustcontain('new file 10644')
592 # TODO(marcink): SVN doesn't yet detect binary changes
593 else:
594 response.mustcontain('new file 100644')
595 response.mustcontain('binary diff hidden')
614
596
615 def test_diff_2way(self, backend):
597 def test_diff_2way(self, backend):
616 commit1 = backend.repo.get_commit(commit_idx=-1)
598 commit1 = backend.repo.get_commit(commit_idx=-1)
@@ -622,14 +604,15 b' class TestFilesDiff:'
622 repo_name=backend.repo_name,
604 repo_name=backend.repo_name,
623 f_path='README'),
605 f_path='README'),
624 params={
606 params={
625 'diff1': commit1.raw_id,
607 'diff1': commit2.raw_id,
626 'diff2': commit2.raw_id,
608 'diff2': commit1.raw_id,
627 })
609 })
610 # use redirect since this is OLD view redirecting to compare page
611 response = response.follow()
628
612
629 # Expecting links to both variants of the file. Links are used
613 # It's a symlink to README.rst
630 # to load the content dynamically.
614 response.mustcontain('README.rst')
631 response.mustcontain('/%s/README' % commit1.raw_id)
615 response.mustcontain('No newline at end of file')
632 response.mustcontain('/%s/README' % commit2.raw_id)
633
616
634 def test_requires_one_commit_id(self, backend, autologin_user):
617 def test_requires_one_commit_id(self, backend, autologin_user):
635 response = self.app.get(
618 response = self.app.get(
@@ -642,21 +625,23 b' class TestFilesDiff:'
642 response.mustcontain(
625 response.mustcontain(
643 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
626 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
644
627
645 def test_returns_not_found_if_file_does_not_exist(self, vcsbackend):
628 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
646 repo = vcsbackend.repo
629 repo = vcsbackend.repo
647 self.app.get(
630 response = self.app.get(
648 url(
631 url(
649 controller='files',
632 controller='files',
650 action='diff',
633 action='diff',
651 repo_name=repo.name,
634 repo_name=repo.name,
652 f_path='does-not-exist-in-any-commit',
635 f_path='does-not-exist-in-any-commit',
653 diff1=repo[0].raw_id,
636 diff1=repo[0].raw_id,
654 diff2=repo[1].raw_id),
637 diff2=repo[1].raw_id),)
655 status=404)
638
639 response = response.follow()
640 response.mustcontain('No files')
656
641
657 def test_returns_redirect_if_file_not_changed(self, backend):
642 def test_returns_redirect_if_file_not_changed(self, backend):
658 commit = backend.repo.get_commit(commit_idx=-1)
643 commit = backend.repo.get_commit(commit_idx=-1)
659 f_path= 'README'
644 f_path = 'README'
660 response = self.app.get(
645 response = self.app.get(
661 url(
646 url(
662 controller='files',
647 controller='files',
@@ -666,25 +651,40 b' class TestFilesDiff:'
666 diff1=commit.raw_id,
651 diff1=commit.raw_id,
667 diff2=commit.raw_id,
652 diff2=commit.raw_id,
668 ),
653 ),
669 status=302
670 )
654 )
671 assert response.headers['Location'].endswith(f_path)
655 response = response.follow()
672 redirected = response.follow()
656 response.mustcontain('No files')
673 redirected.mustcontain('has not changed between')
657 response.mustcontain('No commits in this compare')
674
658
675 def test_supports_diff_to_different_path_svn(self, backend_svn):
659 def test_supports_diff_to_different_path_svn(self, backend_svn):
660 #TODO: check this case
661 return
662
676 repo = backend_svn['svn-simple-layout'].scm_instance()
663 repo = backend_svn['svn-simple-layout'].scm_instance()
677 commit_id = repo[-1].raw_id
664 commit_id_1 = '24'
665 commit_id_2 = '26'
666
667
668 print( url(
669 controller='files',
670 action='diff',
671 repo_name=repo.name,
672 f_path='trunk/example.py',
673 diff1='tags/v0.2/example.py@' + commit_id_1,
674 diff2=commit_id_2))
675
678 response = self.app.get(
676 response = self.app.get(
679 url(
677 url(
680 controller='files',
678 controller='files',
681 action='diff',
679 action='diff',
682 repo_name=repo.name,
680 repo_name=repo.name,
683 f_path='trunk/example.py',
681 f_path='trunk/example.py',
684 diff1='tags/v0.2/example.py@' + commit_id,
682 diff1='tags/v0.2/example.py@' + commit_id_1,
685 diff2=commit_id),
683 diff2=commit_id_2))
686 status=200)
684
685 response = response.follow()
687 response.mustcontain(
686 response.mustcontain(
687 # diff contains this
688 "Will print out a useful message on invocation.")
688 "Will print out a useful message on invocation.")
689
689
690 # Note: Expecting that we indicate the user what's being compared
690 # Note: Expecting that we indicate the user what's being compared
@@ -692,6 +692,9 b' class TestFilesDiff:'
692 response.mustcontain("tags/v0.2/example.py")
692 response.mustcontain("tags/v0.2/example.py")
693
693
694 def test_show_rev_redirects_to_svn_path(self, backend_svn):
694 def test_show_rev_redirects_to_svn_path(self, backend_svn):
695 #TODO: check this case
696 return
697
695 repo = backend_svn['svn-simple-layout'].scm_instance()
698 repo = backend_svn['svn-simple-layout'].scm_instance()
696 commit_id = repo[-1].raw_id
699 commit_id = repo[-1].raw_id
697 response = self.app.get(
700 response = self.app.get(
@@ -708,6 +711,9 b' class TestFilesDiff:'
708 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
711 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
709
712
710 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
713 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
714 #TODO: check this case
715 return
716
711 repo = backend_svn['svn-simple-layout'].scm_instance()
717 repo = backend_svn['svn-simple-layout'].scm_instance()
712 commit_id = repo[-1].raw_id
718 commit_id = repo[-1].raw_id
713 response = self.app.get(
719 response = self.app.get(
@@ -979,100 +985,3 b' def _assert_items_in_response(response, '
979 def assert_timeago_in_response(response, items, params):
985 def assert_timeago_in_response(response, items, params):
980 for item in items:
986 for item in items:
981 response.mustcontain(h.age_component(params['date']))
987 response.mustcontain(h.age_component(params['date']))
982
983
984
985 @pytest.mark.usefixtures("autologin_user", "app")
986 class TestSideBySideDiff:
987
988 def test_diff2way(self, app, backend, backend_stub):
989 f_path = 'content'
990 commit1_content = 'content-25d7e49c18b159446c'
991 commit2_content = 'content-603d6c72c46d953420'
992 repo = backend.create_repo()
993
994 commit1 = _commit_change(
995 repo.repo_name, filename=f_path, content=commit1_content,
996 message='A', vcs_type=backend.alias, parent=None, newfile=True)
997
998 commit2 = _commit_change(
999 repo.repo_name, filename=f_path, content=commit2_content,
1000 message='B, child of A', vcs_type=backend.alias, parent=commit1)
1001
1002 response = self.app.get(url(
1003 controller='files', action='diff_2way',
1004 repo_name=repo.repo_name,
1005 diff1=commit1.raw_id,
1006 diff2=commit2.raw_id,
1007 f_path=f_path))
1008
1009 assert_response = AssertResponse(response)
1010 response.mustcontain(
1011 ('Side-by-side Diff r0:%s ... r1:%s') % ( commit1.short_id, commit2.short_id ))
1012 response.mustcontain('id="compare"')
1013 response.mustcontain((
1014 "var orig1_url = '/%s/raw/%s/%s';\n"
1015 "var orig2_url = '/%s/raw/%s/%s';") %
1016 ( repo.repo_name, commit1.raw_id, f_path,
1017 repo.repo_name, commit2.raw_id, f_path))
1018
1019
1020 def test_diff2way_with_empty_file(self, app, backend, backend_stub):
1021 commits = [
1022 {'message': 'First commit'},
1023 {'message': 'Commit with binary',
1024 'added': [nodes.FileNode('file.empty', content='')]},
1025 ]
1026 f_path='file.empty'
1027 repo = backend.create_repo(commits=commits)
1028 commit_id1 = repo.get_commit(commit_idx=0).raw_id
1029 commit_id2 = repo.get_commit(commit_idx=1).raw_id
1030
1031 response = self.app.get(url(
1032 controller='files', action='diff_2way',
1033 repo_name=repo.repo_name,
1034 diff1=commit_id1,
1035 diff2=commit_id2,
1036 f_path=f_path))
1037
1038 assert_response = AssertResponse(response)
1039 if backend.alias == 'svn':
1040 assert_session_flash( response,
1041 ('%(file_path)s has not changed') % { 'file_path': 'file.empty' })
1042 else:
1043 response.mustcontain(
1044 ('Side-by-side Diff r0:%s ... r1:%s') % ( repo.get_commit(commit_idx=0).short_id, repo.get_commit(commit_idx=1).short_id ))
1045 response.mustcontain('id="compare"')
1046 response.mustcontain((
1047 "var orig1_url = '/%s/raw/%s/%s';\n"
1048 "var orig2_url = '/%s/raw/%s/%s';") %
1049 ( repo.repo_name, commit_id1, f_path,
1050 repo.repo_name, commit_id2, f_path))
1051
1052
1053 def test_empty_diff_2way_redirect_to_summary_with_alert(self, app, backend):
1054 commit_id_range = {
1055 'hg': (
1056 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
1057 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
1058 'git': (
1059 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
1060 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
1061 'svn': (
1062 '335',
1063 '337'),
1064 }
1065 f_path = 'setup.py'
1066
1067 commit_ids = commit_id_range[backend.alias]
1068
1069 response = self.app.get(url(
1070 controller='files', action='diff_2way',
1071 repo_name=backend.repo_name,
1072 diff2=commit_ids[0],
1073 diff1=commit_ids[1],
1074 f_path=f_path))
1075
1076 assert_response = AssertResponse(response)
1077 assert_session_flash( response,
1078 ('%(file_path)s has not changed') % { 'file_path': f_path })
@@ -531,6 +531,81 b' DIFF_FIXTURES = ['
531 }),
531 }),
532 ]),
532 ]),
533
533
534 ('svn',
535 'svn_diff_binary_add_file.diff',
536 [('intl.dll', 'A',
537 {'added': 0,
538 'deleted': 0,
539 'binary': False,
540 'ops': {NEW_FILENODE: 'new file 10644',
541 #TODO(Marcink): depends on binary detection on svn patches
542 # BIN_FILENODE: 'binary diff hidden'
543 }
544 }),
545 ]),
546
547 ('svn',
548 'svn_diff_multiple_changes.diff',
549 [('trunk/doc/images/SettingsOverlay.png', 'M',
550 {'added': 0,
551 'deleted': 0,
552 'binary': False,
553 'ops': {MOD_FILENODE: 'modified file',
554 #TODO(Marcink): depends on binary detection on svn patches
555 # BIN_FILENODE: 'binary diff hidden'
556 }
557 }),
558 ('trunk/doc/source/de/tsvn_ch04.xml', 'M',
559 {'added': 89,
560 'deleted': 34,
561 'binary': False,
562 'ops': {MOD_FILENODE: 'modified file'}
563 }),
564 ('trunk/doc/source/en/tsvn_ch04.xml', 'M',
565 {'added': 66,
566 'deleted': 21,
567 'binary': False,
568 'ops': {MOD_FILENODE: 'modified file'}
569 }),
570 ('trunk/src/Changelog.txt', 'M',
571 {'added': 2,
572 'deleted': 0,
573 'binary': False,
574 'ops': {MOD_FILENODE: 'modified file'}
575 }),
576 ('trunk/src/Resources/TortoiseProcENG.rc', 'M',
577 {'added': 19,
578 'deleted': 13,
579 'binary': False,
580 'ops': {MOD_FILENODE: 'modified file'}
581 }),
582 ('trunk/src/TortoiseProc/SetOverlayPage.cpp', 'M',
583 {'added': 16,
584 'deleted': 1,
585 'binary': False,
586 'ops': {MOD_FILENODE: 'modified file'}
587 }),
588 ('trunk/src/TortoiseProc/SetOverlayPage.h', 'M',
589 {'added': 3,
590 'deleted': 0,
591 'binary': False,
592 'ops': {MOD_FILENODE: 'modified file'}
593 }),
594 ('trunk/src/TortoiseProc/resource.h', 'M',
595 {'added': 2,
596 'deleted': 0,
597 'binary': False,
598 'ops': {MOD_FILENODE: 'modified file'}
599 }),
600 ('trunk/src/TortoiseShell/ShellCache.h', 'M',
601 {'added': 50,
602 'deleted': 1,
603 'binary': False,
604 'ops': {MOD_FILENODE: 'modified file'}
605 }),
606 ]),
607
608
534 # TODO: mikhail: do we still need this?
609 # TODO: mikhail: do we still need this?
535 # (
610 # (
536 # 'hg',
611 # 'hg',
@@ -579,7 +654,6 b' DIFF_FIXTURES = ['
579 # 'pylons_app.egg-info/dependency_links.txt', 'A', {
654 # 'pylons_app.egg-info/dependency_links.txt', 'A', {
580 # 'deleted': 0, 'binary': False, 'added': 1, 'ops': {
655 # 'deleted': 0, 'binary': False, 'added': 1, 'ops': {
581 # 1: 'new file 100644'}}),
656 # 1: 'new file 100644'}}),
582 # #TODO:
583 # ]
657 # ]
584 # ),
658 # ),
585 ]
659 ]
@@ -38,6 +38,7 b' from rhodecode.model.db import User, Rep'
38 from rhodecode.model.meta import Session
38 from rhodecode.model.meta import Session
39 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.scm import ScmModel
40 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
40 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
41 from rhodecode.lib.vcs.backends.base import EmptyCommit
41
42
42
43
43 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
@@ -372,3 +373,37 b' def repo_on_filesystem(repo_name):'
372 repo = vcs.get_vcs_instance(
373 repo = vcs.get_vcs_instance(
373 os.path.join(TESTS_TMP_PATH, repo_name), create=False)
374 os.path.join(TESTS_TMP_PATH, repo_name), create=False)
374 return repo is not None
375 return repo is not None
376
377
378 def commit_change(
379 repo, filename, content, message, vcs_type, parent=None, newfile=False):
380 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
381
382 repo = Repository.get_by_repo_name(repo)
383 _commit = parent
384 if not parent:
385 _commit = EmptyCommit(alias=vcs_type)
386
387 if newfile:
388 nodes = {
389 filename: {
390 'content': content
391 }
392 }
393 commit = ScmModel().create_nodes(
394 user=TEST_USER_ADMIN_LOGIN, repo=repo,
395 message=message,
396 nodes=nodes,
397 parent_commit=_commit,
398 author=TEST_USER_ADMIN_LOGIN,
399 )
400 else:
401 commit = ScmModel().commit_change(
402 repo=repo.scm_instance(), repo_name=repo.repo_name,
403 commit=parent, user=TEST_USER_ADMIN_LOGIN,
404 author=TEST_USER_ADMIN_LOGIN,
405 message=message,
406 content=content,
407 f_path=filename
408 )
409 return commit
@@ -359,14 +359,15 b' class TestSvnGetDiff:'
359 ], ids=['file', 'dir'])
359 ], ids=['file', 'dir'])
360 def test_diff_to_tagged_version(self, vcsbackend_svn, path, path1):
360 def test_diff_to_tagged_version(self, vcsbackend_svn, path, path1):
361 repo = vcsbackend_svn['svn-simple-layout']
361 repo = vcsbackend_svn['svn-simple-layout']
362 commit = repo[-1]
362 commit1 = repo[-2]
363 diff = repo.get_diff(commit, commit, path=path, path1=path1)
363 commit2 = repo[-1]
364 diff = repo.get_diff(commit1, commit2, path=path, path1=path1)
364 assert diff.raw == self.expected_diff_v_0_2
365 assert diff.raw == self.expected_diff_v_0_2
365
366
366 expected_diff_v_0_2 = '''Index: example.py
367 expected_diff_v_0_2 = '''Index: example.py
367 ===================================================================
368 ===================================================================
368 diff --git a/example.py b/example.py
369 diff --git a/example.py b/example.py
369 --- a/example.py\t(revision 26)
370 --- a/example.py\t(revision 25)
370 +++ b/example.py\t(revision 26)
371 +++ b/example.py\t(revision 26)
371 @@ -7,8 +7,12 @@
372 @@ -7,8 +7,12 @@
372
373
@@ -1,50 +0,0 b''
1 /* required */
2 .mergely-column textarea { width: 80px; height: 200px; }
3 .mergely-column { float: left; }
4 .mergely-margin { float: left; }
5 .mergely-canvas { float: left; width: 28px; }
6
7 /* resizeable */
8 .mergely-resizer { width: 100%; height: 100%; }
9
10 /* style configuration */
11 .mergely-column { border: 1px solid #ccc; }
12 .mergely-active { border: 1px solid #a3d1ff; }
13
14 .mergely.a,.mergely.d,.mergely.c { color: #000; }
15
16 .mergely.a.rhs.start { border-top: 1px solid #a3d1ff; }
17 .mergely.a.lhs.start.end,
18 .mergely.a.rhs.end { border-bottom: 1px solid #a3d1ff; }
19 .mergely.a.rhs { background-color: #ddeeff; }
20 .mergely.a.lhs.start.end.first { border-bottom: 0; border-top: 1px solid #a3d1ff; }
21
22 .mergely.d.lhs { background-color: #ffe9e9; }
23 .mergely.d.lhs.end,
24 .mergely.d.rhs.start.end { border-bottom: 1px solid #f8e8e8; }
25 .mergely.d.rhs.start.end.first { border-bottom: 0; border-top: 1px solid #f8e8e8; }
26 .mergely.d.lhs.start { border-top: 1px solid #f8e8e8; }
27
28 .mergely.c.lhs,
29 .mergely.c.rhs { background-color: #fafafa; }
30 .mergely.c.lhs.start,
31 .mergely.c.rhs.start { border-top: 1px solid #a3a3a3; }
32 .mergely.c.lhs.end,
33 .mergely.c.rhs.end { border-bottom: 1px solid #a3a3a3; }
34
35 .mergely.ch.a.rhs { background-color: #ddeeff; }
36 .mergely.ch.d.lhs { background-color: #ffe9e9; text-decoration: line-through; color: red !important; }
37
38 .mergely-margin #compare-lhs-margin,
39 .mergely-margin #compare-rhs-margin {
40 cursor: pointer
41 }
42
43 .mergely.current.start { border-top: 1px solid #000 !important; }
44 .mergely.current.end { border-bottom: 1px solid #000 !important; }
45 .mergely.current.lhs.a.start.end,
46 .mergely.current.rhs.d.start.end { border-top: 0 !important; }
47 .mergely.current.CodeMirror-linenumber { color: #F9F9F9; font-weight: bold; background-color: #777; }
48
49 .CodeMirror-linenumber { cursor: pointer; }
50 .CodeMirror-code { color: #717171; } No newline at end of file
This diff has been collapsed as it changes many lines, (1669 lines changed) Show them Hide them
@@ -1,1669 +0,0 b''
1 "use strict";
2
3 (function( window, document, jQuery, CodeMirror ){
4
5 var Mgly = {};
6
7 Mgly.Timer = function(){
8 var self = this;
9 self.start = function() { self.t0 = new Date().getTime(); };
10 self.stop = function() {
11 var t1 = new Date().getTime();
12 var d = t1 - self.t0;
13 self.t0 = t1;
14 return d;
15 };
16 self.start();
17 };
18
19 Mgly.ChangeExpression = new RegExp(/(^(?![><\-])*\d+(?:,\d+)?)([acd])(\d+(?:,\d+)?)/);
20
21 Mgly.DiffParser = function(diff) {
22 var changes = [];
23 var change_id = 0;
24 // parse diff
25 var diff_lines = diff.split(/\n/);
26 for (var i = 0; i < diff_lines.length; ++i) {
27 if (diff_lines[i].length == 0) continue;
28 var change = {};
29 var test = Mgly.ChangeExpression.exec(diff_lines[i]);
30 if (test == null) continue;
31 // lines are zero-based
32 var fr = test[1].split(',');
33 change['lhs-line-from'] = fr[0] - 1;
34 if (fr.length == 1) change['lhs-line-to'] = fr[0] - 1;
35 else change['lhs-line-to'] = fr[1] - 1;
36 var to = test[3].split(',');
37 change['rhs-line-from'] = to[0] - 1;
38 if (to.length == 1) change['rhs-line-to'] = to[0] - 1;
39 else change['rhs-line-to'] = to[1] - 1;
40 change['op'] = test[2];
41 changes[change_id++] = change;
42 }
43 return changes;
44 };
45
46 Mgly.sizeOf = function(obj) {
47 var size = 0, key;
48 for (key in obj) {
49 if (obj.hasOwnProperty(key)) size++;
50 }
51 return size;
52 };
53
54 Mgly.LCS = function(x, y) {
55 this.x = x.replace(/[ ]{1}/g, '\n');
56 this.y = y.replace(/[ ]{1}/g, '\n');
57 };
58
59 jQuery.extend(Mgly.LCS.prototype, {
60 clear: function() { this.ready = 0; },
61 diff: function(added, removed) {
62 var d = new Mgly.diff(this.x, this.y, {ignorews: false});
63 var changes = Mgly.DiffParser(d.normal_form());
64 var li = 0, lj = 0;
65 for (var i = 0; i < changes.length; ++i) {
66 var change = changes[i];
67 if (change.op != 'a') {
68 // find the starting index of the line
69 li = d.getLines('lhs').slice(0, change['lhs-line-from']).join(' ').length;
70 // get the index of the the span of the change
71 lj = change['lhs-line-to'] + 1;
72 // get the changed text
73 var lchange = d.getLines('lhs').slice(change['lhs-line-from'], lj).join(' ');
74 if (change.op == 'd') lchange += ' ';// include the leading space
75 else if (li > 0 && change.op == 'c') li += 1; // ignore leading space if not first word
76 // output the changed index and text
77 removed(li, li + lchange.length);
78 }
79 if (change.op != 'd') {
80 // find the starting index of the line
81 li = d.getLines('rhs').slice(0, change['rhs-line-from']).join(' ').length;
82 // get the index of the the span of the change
83 lj = change['rhs-line-to'] + 1;
84 // get the changed text
85 var rchange = d.getLines('rhs').slice(change['rhs-line-from'], lj).join(' ');
86 if (change.op == 'a') rchange += ' ';// include the leading space
87 else if (li > 0 && change.op == 'c') li += 1; // ignore leading space if not first word
88 // output the changed index and text
89 added(li, li + rchange.length);
90 }
91 }
92 }
93 });
94
95 Mgly.CodeifyText = function(settings) {
96 this._max_code = 0;
97 this._diff_codes = {};
98 this.ctxs = {};
99 this.options = {ignorews: false};
100 jQuery.extend(this, settings);
101 this.lhs = settings.lhs.split('\n');
102 this.rhs = settings.rhs.split('\n');
103 };
104
105 jQuery.extend(Mgly.CodeifyText.prototype, {
106 getCodes: function(side) {
107 if (!this.ctxs.hasOwnProperty(side)) {
108 var ctx = this._diff_ctx(this[side]);
109 this.ctxs[side] = ctx;
110 ctx.codes.length = Object.keys(ctx.codes).length;
111 }
112 return this.ctxs[side].codes;
113 },
114 getLines: function(side) {
115 return this.ctxs[side].lines;
116 },
117 _diff_ctx: function(lines) {
118 var ctx = {i: 0, codes: {}, lines: lines};
119 this._codeify(lines, ctx);
120 return ctx;
121 },
122 _codeify: function(lines, ctx) {
123 var code = this._max_code;
124 for (var i = 0; i < lines.length; ++i) {
125 var line = lines[i];
126 if (this.options.ignorews) {
127 line = line.replace(/\s+/g, '');
128 }
129 var aCode = this._diff_codes[line];
130 if (aCode != undefined) {
131 ctx.codes[i] = aCode;
132 }
133 else {
134 this._max_code++;
135 this._diff_codes[line] = this._max_code;
136 ctx.codes[i] = this._max_code;
137 }
138 }
139 }
140 });
141
142 Mgly.diff = function(lhs, rhs, options) {
143 var opts = jQuery.extend({ignorews: false}, options);
144 this.codeify = new Mgly.CodeifyText({
145 lhs: lhs,
146 rhs: rhs,
147 options: opts
148 });
149 var lhs_ctx = {
150 codes: this.codeify.getCodes('lhs'),
151 modified: {}
152 };
153 var rhs_ctx = {
154 codes: this.codeify.getCodes('rhs'),
155 modified: {}
156 };
157 var max = (lhs_ctx.codes.length + rhs_ctx.codes.length + 1);
158 var vector_d = [];
159 var vector_u = [];
160 this._lcs(lhs_ctx, 0, lhs_ctx.codes.length, rhs_ctx, 0, rhs_ctx.codes.length, vector_u, vector_d);
161 this._optimize(lhs_ctx);
162 this._optimize(rhs_ctx);
163 this.items = this._create_diffs(lhs_ctx, rhs_ctx);
164 };
165
166 jQuery.extend(Mgly.diff.prototype, {
167 changes: function() { return this.items; },
168 getLines: function(side) {
169 return this.codeify.getLines(side);
170 },
171 normal_form: function() {
172 var nf = '';
173 for (var index = 0; index < this.items.length; ++index) {
174 var item = this.items[index];
175 var lhs_str = '';
176 var rhs_str = '';
177 var change = 'c';
178 if (item.lhs_deleted_count == 0 && item.rhs_inserted_count > 0) change = 'a';
179 else if (item.lhs_deleted_count > 0 && item.rhs_inserted_count == 0) change = 'd';
180
181 if (item.lhs_deleted_count == 1) lhs_str = item.lhs_start + 1;
182 else if (item.lhs_deleted_count == 0) lhs_str = item.lhs_start;
183 else lhs_str = (item.lhs_start + 1) + ',' + (item.lhs_start + item.lhs_deleted_count);
184
185 if (item.rhs_inserted_count == 1) rhs_str = item.rhs_start + 1;
186 else if (item.rhs_inserted_count == 0) rhs_str = item.rhs_start;
187 else rhs_str = (item.rhs_start + 1) + ',' + (item.rhs_start + item.rhs_inserted_count);
188 nf += lhs_str + change + rhs_str + '\n';
189
190 var lhs_lines = this.getLines('lhs');
191 var rhs_lines = this.getLines('rhs');
192 if (rhs_lines && lhs_lines) {
193 var i;
194 // if rhs/lhs lines have been retained, output contextual diff
195 for (i = item.lhs_start; i < item.lhs_start + item.lhs_deleted_count; ++i) {
196 nf += '< ' + lhs_lines[i] + '\n';
197 }
198 if (item.rhs_inserted_count && item.lhs_deleted_count) nf += '---\n';
199 for (i = item.rhs_start; i < item.rhs_start + item.rhs_inserted_count; ++i) {
200 nf += '> ' + rhs_lines[i] + '\n';
201 }
202 }
203 }
204 return nf;
205 },
206 _lcs: function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d) {
207 while ( (lhs_lower < lhs_upper) && (rhs_lower < rhs_upper) && (lhs_ctx.codes[lhs_lower] == rhs_ctx.codes[rhs_lower]) ) {
208 ++lhs_lower;
209 ++rhs_lower;
210 }
211 while ( (lhs_lower < lhs_upper) && (rhs_lower < rhs_upper) && (lhs_ctx.codes[lhs_upper - 1] == rhs_ctx.codes[rhs_upper - 1]) ) {
212 --lhs_upper;
213 --rhs_upper;
214 }
215 if (lhs_lower == lhs_upper) {
216 while (rhs_lower < rhs_upper) {
217 rhs_ctx.modified[ rhs_lower++ ] = true;
218 }
219 }
220 else if (rhs_lower == rhs_upper) {
221 while (lhs_lower < lhs_upper) {
222 lhs_ctx.modified[ lhs_lower++ ] = true;
223 }
224 }
225 else {
226 var sms = this._sms(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d);
227 this._lcs(lhs_ctx, lhs_lower, sms.x, rhs_ctx, rhs_lower, sms.y, vector_u, vector_d);
228 this._lcs(lhs_ctx, sms.x, lhs_upper, rhs_ctx, sms.y, rhs_upper, vector_u, vector_d);
229 }
230 },
231 _sms: function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d) {
232 var max = lhs_ctx.codes.length + rhs_ctx.codes.length + 1;
233 var kdown = lhs_lower - rhs_lower;
234 var kup = lhs_upper - rhs_upper;
235 var delta = (lhs_upper - lhs_lower) - (rhs_upper - rhs_lower);
236 var odd = (delta & 1) != 0;
237 var offset_down = max - kdown;
238 var offset_up = max - kup;
239 var maxd = ((lhs_upper - lhs_lower + rhs_upper - rhs_lower) / 2) + 1;
240 vector_d[ offset_down + kdown + 1 ] = lhs_lower;
241 vector_u[ offset_up + kup - 1 ] = lhs_upper;
242 var ret = {x:0,y:0}, d, k, x, y;
243 for (d = 0; d <= maxd; ++d) {
244 for (k = kdown - d; k <= kdown + d; k += 2) {
245 if (k == kdown - d) {
246 x = vector_d[ offset_down + k + 1 ];//down
247 }
248 else {
249 x = vector_d[ offset_down + k - 1 ] + 1;//right
250 if ((k < (kdown + d)) && (vector_d[ offset_down + k + 1 ] >= x)) {
251 x = vector_d[ offset_down + k + 1 ];//down
252 }
253 }
254 y = x - k;
255 // find the end of the furthest reaching forward D-path in diagonal k.
256 while ((x < lhs_upper) && (y < rhs_upper) && (lhs_ctx.codes[x] == rhs_ctx.codes[y])) {
257 x++; y++;
258 }
259 vector_d[ offset_down + k ] = x;
260 // overlap ?
261 if (odd && (kup - d < k) && (k < kup + d)) {
262 if (vector_u[offset_up + k] <= vector_d[offset_down + k]) {
263 ret.x = vector_d[offset_down + k];
264 ret.y = vector_d[offset_down + k] - k;
265 return (ret);
266 }
267 }
268 }
269 // Extend the reverse path.
270 for (k = kup - d; k <= kup + d; k += 2) {
271 // find the only or better starting point
272 if (k == kup + d) {
273 x = vector_u[offset_up + k - 1]; // up
274 } else {
275 x = vector_u[offset_up + k + 1] - 1; // left
276 if ((k > kup - d) && (vector_u[offset_up + k - 1] < x))
277 x = vector_u[offset_up + k - 1]; // up
278 }
279 y = x - k;
280 while ((x > lhs_lower) && (y > rhs_lower) && (lhs_ctx.codes[x - 1] == rhs_ctx.codes[y - 1])) {
281 // diagonal
282 x--;
283 y--;
284 }
285 vector_u[offset_up + k] = x;
286 // overlap ?
287 if (!odd && (kdown - d <= k) && (k <= kdown + d)) {
288 if (vector_u[offset_up + k] <= vector_d[offset_down + k]) {
289 ret.x = vector_d[offset_down + k];
290 ret.y = vector_d[offset_down + k] - k;
291 return (ret);
292 }
293 }
294 }
295 }
296 throw "the algorithm should never come here.";
297 },
298 _optimize: function(ctx) {
299 var start = 0, end = 0;
300 while (start < ctx.codes.length) {
301 while ((start < ctx.codes.length) && (ctx.modified[start] == undefined || ctx.modified[start] == false)) {
302 start++;
303 }
304 end = start;
305 while ((end < ctx.codes.length) && (ctx.modified[end] == true)) {
306 end++;
307 }
308 if ((end < ctx.codes.length) && (ctx.codes[start] == ctx.codes[end])) {
309 ctx.modified[start] = false;
310 ctx.modified[end] = true;
311 }
312 else {
313 start = end;
314 }
315 }
316 },
317 _create_diffs: function(lhs_ctx, rhs_ctx) {
318 var items = [];
319 var lhs_start = 0, rhs_start = 0;
320 var lhs_line = 0, rhs_line = 0;
321
322 while (lhs_line < lhs_ctx.codes.length || rhs_line < rhs_ctx.codes.length) {
323 if ((lhs_line < lhs_ctx.codes.length) && (!lhs_ctx.modified[lhs_line])
324 && (rhs_line < rhs_ctx.codes.length) && (!rhs_ctx.modified[rhs_line])) {
325 // equal lines
326 lhs_line++;
327 rhs_line++;
328 }
329 else {
330 // maybe deleted and/or inserted lines
331 lhs_start = lhs_line;
332 rhs_start = rhs_line;
333
334 while (lhs_line < lhs_ctx.codes.length && (rhs_line >= rhs_ctx.codes.length || lhs_ctx.modified[lhs_line]))
335 lhs_line++;
336
337 while (rhs_line < rhs_ctx.codes.length && (lhs_line >= lhs_ctx.codes.length || rhs_ctx.modified[rhs_line]))
338 rhs_line++;
339
340 if ((lhs_start < lhs_line) || (rhs_start < rhs_line)) {
341 // store a new difference-item
342 items.push({
343 lhs_start: lhs_start,
344 rhs_start: rhs_start,
345 lhs_deleted_count: lhs_line - lhs_start,
346 rhs_inserted_count: rhs_line - rhs_start
347 });
348 }
349 }
350 }
351 return items;
352 }
353 });
354
355 Mgly.mergely = function(el, options) {
356 if (el) {
357 this.init(el, options);
358 }
359 };
360
361 jQuery.extend(Mgly.mergely.prototype, {
362 name: 'mergely',
363 //http://jupiterjs.com/news/writing-the-perfect-jquery-plugin
364 init: function(el, options) {
365 this.diffView = new Mgly.CodeMirrorDiffView(el, options);
366 this.bind(el);
367 },
368 bind: function(el) {
369 this.diffView.bind(el);
370 }
371 });
372
373 Mgly.CodeMirrorDiffView = function(el, options) {
374 CodeMirror.defineExtension('centerOnCursor', function() {
375 var coords = this.cursorCoords(null, 'local');
376 this.scrollTo(null,
377 (coords.y + coords.yBot) / 2 - (this.getScrollerElement().clientHeight / 2));
378 });
379 this.init(el, options);
380 };
381
382 jQuery.extend(Mgly.CodeMirrorDiffView.prototype, {
383 init: function(el, options) {
384 this.settings = {
385 autoupdate: true,
386 autoresize: true,
387 rhs_margin: 'right',
388 wrap_lines: false,
389 line_numbers: true,
390 lcs: true,
391 sidebar: true,
392 viewport: false,
393 ignorews: false,
394 fadein: 'fast',
395 editor_width: '650px',
396 editor_height: '400px',
397 resize_timeout: 500,
398 change_timeout: 150,
399 fgcolor: {a:'#4ba3fa',c:'#a3a3a3',d:'#ff7f7f', // color for differences (soft color)
400 ca:'#4b73ff',cc:'#434343',cd:'#ff4f4f'}, // color for currently active difference (bright color)
401 bgcolor: '#eee',
402 vpcolor: 'rgba(0, 0, 200, 0.5)',
403 lhs: function(setValue) { },
404 rhs: function(setValue) { },
405 loaded: function() { },
406 _auto_width: function(w) { return w; },
407 resize: function(init) {
408 var scrollbar = init ? 16 : 0;
409 var w = jQuery(el).parent().width() + scrollbar, h = 0;
410 if (this.width == 'auto') {
411 w = this._auto_width(w);
412 }
413 else {
414 w = this.width;
415 this.editor_width = w;
416 }
417 if (this.height == 'auto') {
418 //h = this._auto_height(h);
419 h = jQuery(el).parent().height();
420 }
421 else {
422 h = this.height;
423 this.editor_height = h;
424 }
425 var content_width = w / 2.0 - 2 * 8 - 8;
426 var content_height = h;
427 var self = jQuery(el);
428 self.find('.mergely-column').css({ width: content_width + 'px' });
429 self.find('.mergely-column, .mergely-canvas, .mergely-margin, .mergely-column textarea, .CodeMirror-scroll, .cm-s-default').css({ height: content_height + 'px' });
430 self.find('.mergely-canvas').css({ height: content_height + 'px' });
431 self.find('.mergely-column textarea').css({ width: content_width + 'px' });
432 self.css({ width: w, height: h, clear: 'both' });
433 if (self.css('display') == 'none') {
434 if (this.fadein != false) self.fadeIn(this.fadein);
435 else self.show();
436 if (this.loaded) this.loaded();
437 }
438 if (this.resized) this.resized();
439 },
440 _debug: '', //scroll,draw,calc,diff,markup,change
441 resized: function() { }
442 };
443 var cmsettings = {
444 mode: 'text/plain',
445 readOnly: false,
446 lineWrapping: this.settings.wrap_lines,
447 lineNumbers: this.settings.line_numbers,
448 gutters: ['merge', 'CodeMirror-linenumbers']
449 };
450 this.lhs_cmsettings = {};
451 this.rhs_cmsettings = {};
452
453 // save this element for faster queries
454 this.element = jQuery(el);
455
456 // save options if there are any
457 if (options && options.cmsettings) jQuery.extend(this.lhs_cmsettings, cmsettings, options.cmsettings, options.lhs_cmsettings);
458 if (options && options.cmsettings) jQuery.extend(this.rhs_cmsettings, cmsettings, options.cmsettings, options.rhs_cmsettings);
459 //if (options) jQuery.extend(this.settings, options);
460
461 // bind if the element is destroyed
462 this.element.bind('destroyed', jQuery.proxy(this.teardown, this));
463
464 // save this instance in jQuery data, binding this view to the node
465 jQuery.data(el, 'mergely', this);
466
467 this._setOptions(options);
468 },
469 unbind: function() {
470 if (this.changed_timeout != null) clearTimeout(this.changed_timeout);
471 this.editor[this.id + '-lhs'].toTextArea();
472 this.editor[this.id + '-rhs'].toTextArea();
473 jQuery(window).off('.mergely');
474 },
475 destroy: function() {
476 this.element.unbind('destroyed', this.teardown);
477 this.teardown();
478 },
479 teardown: function() {
480 this.unbind();
481 },
482 lhs: function(text) {
483 this.editor[this.id + '-lhs'].setValue(text);
484 },
485 rhs: function(text) {
486 this.editor[this.id + '-rhs'].setValue(text);
487 },
488 update: function() {
489 this._changing(this.id + '-lhs', this.id + '-rhs');
490 },
491 unmarkup: function() {
492 this._clear();
493 },
494 scrollToDiff: function(direction) {
495 if (!this.changes.length) return;
496 if (direction == 'next') {
497 this._current_diff = Math.min(++this._current_diff, this.changes.length - 1);
498 }
499 else if (direction == 'prev') {
500 this._current_diff = Math.max(--this._current_diff, 0);
501 }
502 this._scroll_to_change(this.changes[this._current_diff]);
503 this._changed(this.id + '-lhs', this.id + '-rhs');
504 },
505 mergeCurrentChange: function(side) {
506 if (!this.changes.length) return;
507 if (side == 'lhs' && !this.lhs_cmsettings.readOnly) {
508 this._merge_change(this.changes[this._current_diff], 'rhs', 'lhs');
509 }
510 else if (side == 'rhs' && !this.rhs_cmsettings.readOnly) {
511 this._merge_change(this.changes[this._current_diff], 'lhs', 'rhs');
512 }
513 },
514 scrollTo: function(side, num) {
515 var le = this.editor[this.id + '-lhs'];
516 var re = this.editor[this.id + '-rhs'];
517 if (side == 'lhs') {
518 le.setCursor(num);
519 le.centerOnCursor();
520 }
521 else {
522 re.setCursor(num);
523 re.centerOnCursor();
524 }
525 },
526 _setOptions: function(opts) {
527 jQuery.extend(this.settings, opts);
528 if (this.settings.hasOwnProperty('rhs_margin')) {
529 // dynamically swap the margin
530 if (this.settings.rhs_margin == 'left') {
531 this.element.find('.mergely-margin:last-child').insertAfter(
532 this.element.find('.mergely-canvas'));
533 }
534 else {
535 var target = this.element.find('.mergely-margin').last();
536 target.appendTo(target.parent());
537 }
538 }
539 if (this.settings.hasOwnProperty('sidebar')) {
540 // dynamically enable sidebars
541 if (this.settings.sidebar) {
542 this.element.find('.mergely-margin').css({display: 'block'});
543 }
544 else {
545 this.element.find('.mergely-margin').css({display: 'none'});
546 }
547 }
548 var le, re;
549 if (this.settings.hasOwnProperty('wrap_lines')) {
550 if (this.editor) {
551 le = this.editor[this.id + '-lhs'];
552 re = this.editor[this.id + '-rhs'];
553 le.setOption('lineWrapping', this.settings.wrap_lines);
554 re.setOption('lineWrapping', this.settings.wrap_lines);
555 }
556 }
557 if (this.settings.hasOwnProperty('line_numbers')) {
558 if (this.editor) {
559 le = this.editor[this.id + '-lhs'];
560 re = this.editor[this.id + '-rhs'];
561 le.setOption('lineNumbers', this.settings.line_numbers);
562 re.setOption('lineNumbers', this.settings.line_numbers);
563 }
564 }
565 },
566 options: function(opts) {
567 if (opts) {
568 this._setOptions(opts);
569 if (this.settings.autoresize) this.resize();
570 if (this.settings.autoupdate) this.update();
571 }
572 else {
573 return this.settings;
574 }
575 },
576 swap: function() {
577 if (this.lhs_cmsettings.readOnly || this.rhs_cmsettings.readOnly) return;
578 var le = this.editor[this.id + '-lhs'];
579 var re = this.editor[this.id + '-rhs'];
580 var tmp = re.getValue();
581 re.setValue(le.getValue());
582 le.setValue(tmp);
583 },
584 merge: function(side) {
585 var le = this.editor[this.id + '-lhs'];
586 var re = this.editor[this.id + '-rhs'];
587 if (side == 'lhs' && !this.lhs_cmsettings.readOnly) le.setValue(re.getValue());
588 else if (!this.rhs_cmsettings.readOnly) re.setValue(le.getValue());
589 },
590 get: function(side) {
591 var ed = this.editor[this.id + '-' + side];
592 var t = ed.getValue();
593 if (t == undefined) return '';
594 return t;
595 },
596 clear: function(side) {
597 if (side == 'lhs' && this.lhs_cmsettings.readOnly) return;
598 if (side == 'rhs' && this.rhs_cmsettings.readOnly) return;
599 var ed = this.editor[this.id + '-' + side];
600 ed.setValue('');
601 },
602 cm: function(side) {
603 return this.editor[this.id + '-' + side];
604 },
605 search: function(side, query, direction) {
606 var le = this.editor[this.id + '-lhs'];
607 var re = this.editor[this.id + '-rhs'];
608 var editor;
609 if (side == 'lhs') editor = le;
610 else editor = re;
611 direction = (direction == 'prev') ? 'findPrevious' : 'findNext';
612 if ((editor.getSelection().length == 0) || (this.prev_query[side] != query)) {
613 this.cursor[this.id] = editor.getSearchCursor(query, { line: 0, ch: 0 }, false);
614 this.prev_query[side] = query;
615 }
616 var cursor = this.cursor[this.id];
617
618 if (cursor[direction]()) {
619 editor.setSelection(cursor.from(), cursor.to());
620 }
621 else {
622 cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, false);
623 }
624 },
625 resize: function() {
626 this.settings.resize();
627 this._changing(this.id + '-lhs', this.id + '-rhs');
628 this._set_top_offset(this.id + '-lhs');
629 },
630 diff: function() {
631 var lhs = this.editor[this.id + '-lhs'].getValue();
632 var rhs = this.editor[this.id + '-rhs'].getValue();
633 var d = new Mgly.diff(lhs, rhs, this.settings);
634 return d.normal_form();
635 },
636 bind: function(el) {
637 this.element.hide();//hide
638 this.id = jQuery(el).attr('id');
639 this.changed_timeout = null;
640 this.chfns = {};
641 this.chfns[this.id + '-lhs'] = [];
642 this.chfns[this.id + '-rhs'] = [];
643 this.prev_query = [];
644 this.cursor = [];
645 this._skipscroll = {};
646 this.change_exp = new RegExp(/(\d+(?:,\d+)?)([acd])(\d+(?:,\d+)?)/);
647 var merge_lhs_button;
648 var merge_rhs_button;
649 if (jQuery.button != undefined) {
650 //jquery ui
651 merge_lhs_button = '<button title="Merge left"></button>';
652 merge_rhs_button = '<button title="Merge right"></button>';
653 }
654 else {
655 // homebrew
656 var style = 'opacity:0.4;width:10px;height:15px;background-color:#888;cursor:pointer;text-align:center;color:#eee;border:1px solid: #222;margin-right:5px;margin-top: -2px;';
657 merge_lhs_button = '<div style="' + style + '" title="Merge left">&lt;</div>';
658 merge_rhs_button = '<div style="' + style + '" title="Merge right">&gt;</div>';
659 }
660 this.merge_rhs_button = jQuery(merge_rhs_button);
661 this.merge_lhs_button = jQuery(merge_lhs_button);
662
663 // create the textarea and canvas elements
664 var height = this.settings.editor_height;
665 var width = this.settings.editor_width;
666 this.element.append(jQuery('<div class="mergely-margin" style="height: ' + height + '"><canvas id="' + this.id + '-lhs-margin" width="8px" height="' + height + '"></canvas></div>'));
667 this.element.append(jQuery('<div style="position:relative;width:' + width + '; height:' + height + '" id="' + this.id + '-editor-lhs" class="mergely-column"><textarea style="" id="' + this.id + '-lhs"></textarea></div>'));
668 this.element.append(jQuery('<div class="mergely-canvas" style="height: ' + height + '"><canvas id="' + this.id + '-lhs-' + this.id + '-rhs-canvas" style="width:28px" width="28px" height="' + height + '"></canvas></div>'));
669 var rmargin = jQuery('<div class="mergely-margin" style="height: ' + height + '"><canvas id="' + this.id + '-rhs-margin" width="8px" height="' + height + '"></canvas></div>');
670 if (!this.settings.sidebar) {
671 this.element.find('.mergely-margin').css({display: 'none'});
672 }
673 if (this.settings.rhs_margin == 'left') {
674 this.element.append(rmargin);
675 }
676 this.element.append(jQuery('<div style="width:' + width + '; height:' + height + '" id="' + this.id + '-editor-rhs" class="mergely-column"><textarea style="" id="' + this.id + '-rhs"></textarea></div>'));
677 if (this.settings.rhs_margin != 'left') {
678 this.element.append(rmargin);
679 }
680 //codemirror
681 var cmstyle = '#' + this.id + ' .CodeMirror-gutter-text { padding: 5px 0 0 0; }' +
682 '#' + this.id + ' .CodeMirror-lines pre, ' + '#' + this.id + ' .CodeMirror-gutter-text pre { line-height: 18px; }' +
683 '.CodeMirror-linewidget { overflow: hidden; };';
684 if (this.settings.autoresize) {
685 cmstyle += this.id + ' .CodeMirror-scroll { height: 100%; overflow: auto; }';
686 }
687 // adjust the margin line height
688 cmstyle += '\n.CodeMirror { line-height: 18px; }';
689 jQuery('<style type="text/css">' + cmstyle + '</style>').appendTo('head');
690
691 //bind
692 var rhstx = this.element.find('#' + this.id + '-rhs').get(0);
693 if (!rhstx) {
694 console.error('rhs textarea not defined - Mergely not initialized properly');
695 return;
696 }
697 var lhstx = this.element.find('#' + this.id + '-lhs').get(0);
698 if (!rhstx) {
699 console.error('lhs textarea not defined - Mergely not initialized properly');
700 return;
701 }
702 var self = this;
703 this.editor = [];
704 this.editor[this.id + '-lhs'] = CodeMirror.fromTextArea(lhstx, this.lhs_cmsettings);
705 this.editor[this.id + '-rhs'] = CodeMirror.fromTextArea(rhstx, this.rhs_cmsettings);
706 this.editor[this.id + '-lhs'].on('change', function(){ if (self.settings.autoupdate) self._changing(self.id + '-lhs', self.id + '-rhs'); });
707 this.editor[this.id + '-lhs'].on('scroll', function(){ self._scrolling(self.id + '-lhs'); });
708 this.editor[this.id + '-rhs'].on('change', function(){ if (self.settings.autoupdate) self._changing(self.id + '-lhs', self.id + '-rhs'); });
709 this.editor[this.id + '-rhs'].on('scroll', function(){ self._scrolling(self.id + '-rhs'); });
710 // resize
711 if (this.settings.autoresize) {
712 var sz_timeout1 = null;
713 var sz = function(init) {
714 //self.em_height = null; //recalculate
715 if (self.settings.resize) self.settings.resize(init);
716 self.editor[self.id + '-lhs'].refresh();
717 self.editor[self.id + '-rhs'].refresh();
718 if (self.settings.autoupdate) {
719 self._changing(self.id + '-lhs', self.id + '-rhs');
720 }
721 };
722 jQuery(window).on('resize.mergely',
723 function () {
724 if (sz_timeout1) clearTimeout(sz_timeout1);
725 sz_timeout1 = setTimeout(sz, self.settings.resize_timeout);
726 }
727 );
728 sz(true);
729 }
730
731 // scrollToDiff() from gutter
732 function gutterClicked(side, line, ev) {
733 // The "Merge left/right" buttons are also located in the gutter.
734 // Don't interfere with them:
735 if (ev.target && (jQuery(ev.target).closest('.merge-button').length > 0)) {
736 return;
737 }
738
739 // See if the user clicked the line number of a difference:
740 var i, change;
741 for (i = 0; i < this.changes.length; i++) {
742 change = this.changes[i];
743 if (line >= change[side+'-line-from'] && line <= change[side+'-line-to']) {
744 this._current_diff = i;
745 // I really don't like this here - something about gutterClick does not
746 // like mutating editor here. Need to trigger the scroll to diff from
747 // a timeout.
748 setTimeout(function() { this.scrollToDiff(); }.bind(this), 10);
749 break;
750 }
751 }
752 }
753
754 this.editor[this.id + '-lhs'].on('gutterClick', function(cm, n, gutterClass, ev) {
755 gutterClicked.call(this, 'lhs', n, ev);
756 }.bind(this));
757
758 this.editor[this.id + '-rhs'].on('gutterClick', function(cm, n, gutterClass, ev) {
759 gutterClicked.call(this, 'rhs', n, ev);
760 }.bind(this));
761
762 //bind
763 var setv;
764 if (this.settings.lhs) {
765 setv = this.editor[this.id + '-lhs'].getDoc().setValue;
766 this.settings.lhs(setv.bind(this.editor[this.id + '-lhs'].getDoc()));
767 }
768 if (this.settings.rhs) {
769 setv = this.editor[this.id + '-rhs'].getDoc().setValue;
770 this.settings.rhs(setv.bind(this.editor[this.id + '-rhs'].getDoc()));
771 }
772 },
773
774 _scroll_to_change : function(change) {
775 if (!change) return;
776 var self = this;
777 var led = self.editor[self.id+'-lhs'];
778 var red = self.editor[self.id+'-rhs'];
779 // set cursors
780 led.setCursor(Math.max(change["lhs-line-from"],0), 0); // use led.getCursor().ch ?
781 red.setCursor(Math.max(change["rhs-line-from"],0), 0);
782 led.scrollIntoView({line: change["lhs-line-to"]});
783 },
784
785 _scrolling: function(editor_name) {
786 if (this._skipscroll[editor_name] === true) {
787 // scrolling one side causes the other to event - ignore it
788 this._skipscroll[editor_name] = false;
789 return;
790 }
791 var scroller = jQuery(this.editor[editor_name].getScrollerElement());
792 if (this.midway == undefined) {
793 this.midway = (scroller.height() / 2.0 + scroller.offset().top).toFixed(2);
794 }
795 // balance-line
796 var midline = this.editor[editor_name].coordsChar({left:0, top:this.midway});
797 var top_to = scroller.scrollTop();
798 var left_to = scroller.scrollLeft();
799
800 this.trace('scroll', 'side', editor_name);
801 this.trace('scroll', 'midway', this.midway);
802 this.trace('scroll', 'midline', midline);
803 this.trace('scroll', 'top_to', top_to);
804 this.trace('scroll', 'left_to', left_to);
805
806 var editor_name1 = this.id + '-lhs';
807 var editor_name2 = this.id + '-rhs';
808
809 for (var name in this.editor) {
810 if (!this.editor.hasOwnProperty(name)) continue;
811 if (editor_name == name) continue; //same editor
812 var this_side = editor_name.replace(this.id + '-', '');
813 var other_side = name.replace(this.id + '-', '');
814 var top_adjust = 0;
815
816 // find the last change that is less than or within the midway point
817 // do not move the rhs until the lhs end point is >= the rhs end point.
818 var last_change = null;
819 var force_scroll = false;
820 for (var i = 0; i < this.changes.length; ++i) {
821 var change = this.changes[i];
822 if ((midline.line >= change[this_side+'-line-from'])) {
823 last_change = change;
824 if (midline.line >= last_change[this_side+'-line-to']) {
825 if (!change.hasOwnProperty(this_side+'-y-start') ||
826 !change.hasOwnProperty(this_side+'-y-end') ||
827 !change.hasOwnProperty(other_side+'-y-start') ||
828 !change.hasOwnProperty(other_side+'-y-end')){
829 // change outside of viewport
830 force_scroll = true;
831 }
832 else {
833 top_adjust +=
834 (change[this_side+'-y-end'] - change[this_side+'-y-start']) -
835 (change[other_side+'-y-end'] - change[other_side+'-y-start']);
836 }
837 }
838 }
839 }
840
841 var vp = this.editor[name].getViewport();
842 var scroll = true;
843 if (last_change) {
844 this.trace('scroll', 'last change before midline', last_change);
845 if (midline.line >= vp.from && midline <= vp.to) {
846 scroll = false;
847 }
848 }
849 this.trace('scroll', 'scroll', scroll);
850 if (scroll || force_scroll) {
851 // scroll the other side
852 this.trace('scroll', 'scrolling other side', top_to - top_adjust);
853 this._skipscroll[name] = true;//disable next event
854 this.editor[name].scrollTo(left_to, top_to - top_adjust);
855 }
856 else this.trace('scroll', 'not scrolling other side');
857
858 if (this.settings.autoupdate) {
859 var timer = new Mgly.Timer();
860 this._calculate_offsets(editor_name1, editor_name2, this.changes);
861 this.trace('change', 'offsets time', timer.stop());
862 this._markup_changes(editor_name1, editor_name2, this.changes);
863 this.trace('change', 'markup time', timer.stop());
864 this._draw_diff(editor_name1, editor_name2, this.changes);
865 this.trace('change', 'draw time', timer.stop());
866 }
867 this.trace('scroll', 'scrolled');
868 }
869 },
870 _changing: function(editor_name1, editor_name2) {
871 this.trace('change', 'changing-timeout', this.changed_timeout);
872 var self = this;
873 if (this.changed_timeout != null) clearTimeout(this.changed_timeout);
874 this.changed_timeout = setTimeout(function(){
875 var timer = new Mgly.Timer();
876 self._changed(editor_name1, editor_name2);
877 self.trace('change', 'total time', timer.stop());
878 }, this.settings.change_timeout);
879 },
880 _changed: function(editor_name1, editor_name2) {
881 this._clear();
882 this._diff(editor_name1, editor_name2);
883 },
884 _clear: function() {
885 var self = this, name, editor, fns, timer, i, change, l;
886
887 var clear_changes = function() {
888 timer = new Mgly.Timer();
889 for (i = 0, l = editor.lineCount(); i < l; ++i) {
890 editor.removeLineClass(i, 'background');
891 }
892 for (i = 0; i < fns.length; ++i) {
893 //var edid = editor.getDoc().id;
894 change = fns[i];
895 //if (change.doc.id != edid) continue;
896 if (change.lines.length) {
897 self.trace('change', 'clear text', change.lines[0].text);
898 }
899 change.clear();
900 }
901 editor.clearGutter('merge');
902 self.trace('change', 'clear time', timer.stop());
903 };
904
905 for (name in this.editor) {
906 if (!this.editor.hasOwnProperty(name)) continue;
907 editor = this.editor[name];
908 fns = self.chfns[name];
909 // clear editor changes
910 editor.operation(clear_changes);
911 }
912 self.chfns[name] = [];
913
914 var ex = this._draw_info(this.id + '-lhs', this.id + '-rhs');
915 var ctx_lhs = ex.clhs.get(0).getContext('2d');
916 var ctx_rhs = ex.crhs.get(0).getContext('2d');
917 var ctx = ex.dcanvas.getContext('2d');
918
919 ctx_lhs.beginPath();
920 ctx_lhs.fillStyle = this.settings.bgcolor;
921 ctx_lhs.strokeStyle = '#888';
922 ctx_lhs.fillRect(0, 0, 6.5, ex.visible_page_height);
923 ctx_lhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
924
925 ctx_rhs.beginPath();
926 ctx_rhs.fillStyle = this.settings.bgcolor;
927 ctx_rhs.strokeStyle = '#888';
928 ctx_rhs.fillRect(0, 0, 6.5, ex.visible_page_height);
929 ctx_rhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
930
931 ctx.beginPath();
932 ctx.fillStyle = '#fff';
933 ctx.fillRect(0, 0, this.draw_mid_width, ex.visible_page_height);
934 },
935 _diff: function(editor_name1, editor_name2) {
936 var lhs = this.editor[editor_name1].getValue();
937 var rhs = this.editor[editor_name2].getValue();
938 var timer = new Mgly.Timer();
939 var d = new Mgly.diff(lhs, rhs, this.settings);
940 this.trace('change', 'diff time', timer.stop());
941 this.changes = Mgly.DiffParser(d.normal_form());
942 this.trace('change', 'parse time', timer.stop());
943 if (this._current_diff === undefined && this.changes.length) {
944 // go to first difference on start-up
945 this._current_diff = 0;
946 this._scroll_to_change(this.changes[0]);
947 }
948 this.trace('change', 'scroll_to_change time', timer.stop());
949 this._calculate_offsets(editor_name1, editor_name2, this.changes);
950 this.trace('change', 'offsets time', timer.stop());
951 this._markup_changes(editor_name1, editor_name2, this.changes);
952 this.trace('change', 'markup time', timer.stop());
953 this._draw_diff(editor_name1, editor_name2, this.changes);
954 this.trace('change', 'draw time', timer.stop());
955 },
956 _parse_diff: function (editor_name1, editor_name2, diff) {
957 this.trace('diff', 'diff results:\n', diff);
958 var changes = [];
959 var change_id = 0;
960 // parse diff
961 var diff_lines = diff.split(/\n/);
962 for (var i = 0; i < diff_lines.length; ++i) {
963 if (diff_lines[i].length == 0) continue;
964 var change = {};
965 var test = this.change_exp.exec(diff_lines[i]);
966 if (test == null) continue;
967 // lines are zero-based
968 var fr = test[1].split(',');
969 change['lhs-line-from'] = fr[0] - 1;
970 if (fr.length == 1) change['lhs-line-to'] = fr[0] - 1;
971 else change['lhs-line-to'] = fr[1] - 1;
972 var to = test[3].split(',');
973 change['rhs-line-from'] = to[0] - 1;
974 if (to.length == 1) change['rhs-line-to'] = to[0] - 1;
975 else change['rhs-line-to'] = to[1] - 1;
976 // TODO: optimize for changes that are adds/removes
977 if (change['lhs-line-from'] < 0) change['lhs-line-from'] = 0;
978 if (change['lhs-line-to'] < 0) change['lhs-line-to'] = 0;
979 if (change['rhs-line-from'] < 0) change['rhs-line-from'] = 0;
980 if (change['rhs-line-to'] < 0) change['rhs-line-to'] = 0;
981 change['op'] = test[2];
982 changes[change_id++] = change;
983 this.trace('diff', 'change', change);
984 }
985 return changes;
986 },
987 _get_viewport: function(editor_name1, editor_name2) {
988 var lhsvp = this.editor[editor_name1].getViewport();
989 var rhsvp = this.editor[editor_name2].getViewport();
990 return {from: Math.min(lhsvp.from, rhsvp.from), to: Math.max(lhsvp.to, rhsvp.to)};
991 },
992 _is_change_in_view: function(vp, change) {
993 if (!this.settings.viewport) return true;
994 if ((change['lhs-line-from'] < vp.from && change['lhs-line-to'] < vp.to) ||
995 (change['lhs-line-from'] > vp.from && change['lhs-line-to'] > vp.to) ||
996 (change['rhs-line-from'] < vp.from && change['rhs-line-to'] < vp.to) ||
997 (change['rhs-line-from'] > vp.from && change['rhs-line-to'] > vp.to)) {
998 // if the change is outside the viewport, skip
999 return false;
1000 }
1001 return true;
1002 },
1003 _set_top_offset: function (editor_name1) {
1004 // save the current scroll position of the editor
1005 var saveY = this.editor[editor_name1].getScrollInfo().top;
1006 // temporarily scroll to top
1007 this.editor[editor_name1].scrollTo(null, 0);
1008
1009 // this is the distance from the top of the screen to the top of the
1010 // content of the first codemirror editor
1011 var topnode = this.element.find('.CodeMirror-measure').first();
1012 var top_offset = topnode.offset().top - 4;
1013 if(!top_offset) return false;
1014
1015 // restore editor's scroll position
1016 this.editor[editor_name1].scrollTo(null, saveY);
1017
1018 this.draw_top_offset = 0.5 - top_offset;
1019 return true;
1020 },
1021 _calculate_offsets: function (editor_name1, editor_name2, changes) {
1022 if (this.em_height == null) {
1023 if(!this._set_top_offset(editor_name1)) return; //try again
1024 this.em_height = this.editor[editor_name1].defaultTextHeight();
1025 if (!this.em_height) {
1026 console.warn('Failed to calculate offsets, using 18 by default');
1027 this.em_height = 18;
1028 }
1029 this.draw_lhs_min = 0.5;
1030 var c = jQuery('#' + editor_name1 + '-' + editor_name2 + '-canvas');
1031 if (!c.length) {
1032 console.error('failed to find canvas', '#' + editor_name1 + '-' + editor_name2 + '-canvas');
1033 }
1034 if (!c.width()) {
1035 console.error('canvas width is 0');
1036 return;
1037 }
1038 this.draw_mid_width = jQuery('#' + editor_name1 + '-' + editor_name2 + '-canvas').width();
1039 this.draw_rhs_max = this.draw_mid_width - 0.5; //24.5;
1040 this.draw_lhs_width = 5;
1041 this.draw_rhs_width = 5;
1042 this.trace('calc', 'change offsets calculated', {top_offset: this.draw_top_offset, lhs_min: this.draw_lhs_min, rhs_max: this.draw_rhs_max, lhs_width: this.draw_lhs_width, rhs_width: this.draw_rhs_width});
1043 }
1044 var lhschc = this.editor[editor_name1].charCoords({line: 0});
1045 var rhschc = this.editor[editor_name2].charCoords({line: 0});
1046 var vp = this._get_viewport(editor_name1, editor_name2);
1047
1048 for (var i = 0; i < changes.length; ++i) {
1049 var change = changes[i];
1050
1051 if (!this.settings.sidebar && !this._is_change_in_view(vp, change)) {
1052 // if the change is outside the viewport, skip
1053 delete change['lhs-y-start'];
1054 delete change['lhs-y-end'];
1055 delete change['rhs-y-start'];
1056 delete change['rhs-y-end'];
1057 continue;
1058 }
1059 var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
1060 var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
1061 var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
1062 var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
1063
1064 var ls, le, rs, re, tls, tle, lhseh, lhssh, rhssh, rhseh;
1065 if (this.editor[editor_name1].getOption('lineWrapping') || this.editor[editor_name2].getOption('lineWrapping')) {
1066 // If using line-wrapping, we must get the height of the line
1067 tls = this.editor[editor_name1].cursorCoords({line: llf, ch: 0}, 'page');
1068 lhssh = this.editor[editor_name1].getLineHandle(llf);
1069 ls = { top: tls.top, bottom: tls.top + lhssh.height };
1070
1071 tle = this.editor[editor_name1].cursorCoords({line: llt, ch: 0}, 'page');
1072 lhseh = this.editor[editor_name1].getLineHandle(llt);
1073 le = { top: tle.top, bottom: tle.top + lhseh.height };
1074
1075 tls = this.editor[editor_name2].cursorCoords({line: rlf, ch: 0}, 'page');
1076 rhssh = this.editor[editor_name2].getLineHandle(rlf);
1077 rs = { top: tls.top, bottom: tls.top + rhssh.height };
1078
1079 tle = this.editor[editor_name2].cursorCoords({line: rlt, ch: 0}, 'page');
1080 rhseh = this.editor[editor_name2].getLineHandle(rlt);
1081 re = { top: tle.top, bottom: tle.top + rhseh.height };
1082 }
1083 else {
1084 // If not using line-wrapping, we can calculate the line position
1085 ls = {
1086 top: lhschc.top + llf * this.em_height,
1087 bottom: lhschc.bottom + llf * this.em_height + 2
1088 };
1089 le = {
1090 top: lhschc.top + llt * this.em_height,
1091 bottom: lhschc.bottom + llt * this.em_height + 2
1092 };
1093 rs = {
1094 top: rhschc.top + rlf * this.em_height,
1095 bottom: rhschc.bottom + rlf * this.em_height + 2
1096 };
1097 re = {
1098 top: rhschc.top + rlt * this.em_height,
1099 bottom: rhschc.bottom + rlt * this.em_height + 2
1100 };
1101 }
1102
1103 if (change['op'] == 'a') {
1104 // adds (right), normally start from the end of the lhs,
1105 // except for the case when the start of the rhs is 0
1106 if (rlf > 0) {
1107 ls.top = ls.bottom;
1108 ls.bottom += this.em_height;
1109 le = ls;
1110 }
1111 }
1112 else if (change['op'] == 'd') {
1113 // deletes (left) normally finish from the end of the rhs,
1114 // except for the case when the start of the lhs is 0
1115 if (llf > 0) {
1116 rs.top = rs.bottom;
1117 rs.bottom += this.em_height;
1118 re = rs;
1119 }
1120 }
1121 change['lhs-y-start'] = this.draw_top_offset + ls.top;
1122 if (change['op'] == 'c' || change['op'] == 'd') {
1123 change['lhs-y-end'] = this.draw_top_offset + le.bottom;
1124 }
1125 else {
1126 change['lhs-y-end'] = this.draw_top_offset + le.top;
1127 }
1128 change['rhs-y-start'] = this.draw_top_offset + rs.top;
1129 if (change['op'] == 'c' || change['op'] == 'a') {
1130 change['rhs-y-end'] = this.draw_top_offset + re.bottom;
1131 }
1132 else {
1133 change['rhs-y-end'] = this.draw_top_offset + re.top;
1134 }
1135 this.trace('calc', 'change calculated', i, change);
1136 }
1137 return changes;
1138 },
1139 _markup_changes: function (editor_name1, editor_name2, changes) {
1140 this.element.find('.merge-button').remove(); //clear
1141
1142 var self = this;
1143 var led = this.editor[editor_name1];
1144 var red = this.editor[editor_name2];
1145 var current_diff = this._current_diff;
1146
1147 var timer = new Mgly.Timer();
1148 led.operation(function() {
1149 for (var i = 0; i < changes.length; ++i) {
1150 var change = changes[i];
1151 var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
1152 var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
1153 var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
1154 var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
1155
1156 var clazz = ['mergely', 'lhs', change['op'], 'cid-' + i];
1157 led.addLineClass(llf, 'background', 'start');
1158 led.addLineClass(llt, 'background', 'end');
1159
1160 if (current_diff == i) {
1161 if (llf != llt) {
1162 led.addLineClass(llf, 'background', 'current');
1163 }
1164 led.addLineClass(llt, 'background', 'current');
1165 }
1166 if (llf == 0 && llt == 0 && rlf == 0) {
1167 led.addLineClass(llf, 'background', clazz.join(' '));
1168 led.addLineClass(llf, 'background', 'first');
1169 }
1170 else {
1171 // apply change for each line in-between the changed lines
1172 for (var j = llf; j <= llt; ++j) {
1173 led.addLineClass(j, 'background', clazz.join(' '));
1174 led.addLineClass(j, 'background', clazz.join(' '));
1175 }
1176 }
1177
1178 if (!red.getOption('readOnly')) {
1179 // add widgets to lhs, if rhs is not read only
1180 var rhs_button = self.merge_rhs_button.clone();
1181 if (rhs_button.button) {
1182 //jquery-ui support
1183 rhs_button.button({icons: {primary: 'ui-icon-triangle-1-e'}, text: false});
1184 }
1185 rhs_button.addClass('merge-button');
1186 rhs_button.attr('id', 'merge-rhs-' + i);
1187 led.setGutterMarker(llf, 'merge', rhs_button.get(0));
1188 }
1189 }
1190 });
1191
1192 var vp = this._get_viewport(editor_name1, editor_name2);
1193
1194 this.trace('change', 'markup lhs-editor time', timer.stop());
1195 red.operation(function() {
1196 for (var i = 0; i < changes.length; ++i) {
1197 var change = changes[i];
1198 var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
1199 var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
1200 var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
1201 var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
1202
1203 if (!self._is_change_in_view(vp, change)) {
1204 // if the change is outside the viewport, skip
1205 continue;
1206 }
1207
1208 var clazz = ['mergely', 'rhs', change['op'], 'cid-' + i];
1209 red.addLineClass(rlf, 'background', 'start');
1210 red.addLineClass(rlt, 'background', 'end');
1211
1212 if (current_diff == i) {
1213 if (rlf != rlt) {
1214 red.addLineClass(rlf, 'background', 'current');
1215 }
1216 red.addLineClass(rlt, 'background', 'current');
1217 }
1218 if (rlf == 0 && rlt == 0 && llf == 0) {
1219 red.addLineClass(rlf, 'background', clazz.join(' '));
1220 red.addLineClass(rlf, 'background', 'first');
1221 }
1222 else {
1223 // apply change for each line in-between the changed lines
1224 for (var j = rlf; j <= rlt; ++j) {
1225 red.addLineClass(j, 'background', clazz.join(' '));
1226 red.addLineClass(j, 'background', clazz.join(' '));
1227 }
1228 }
1229
1230 if (!led.getOption('readOnly')) {
1231 // add widgets to rhs, if lhs is not read only
1232 var lhs_button = self.merge_lhs_button.clone();
1233 if (lhs_button.button) {
1234 //jquery-ui support
1235 lhs_button.button({icons: {primary: 'ui-icon-triangle-1-w'}, text: false});
1236 }
1237 lhs_button.addClass('merge-button');
1238 lhs_button.attr('id', 'merge-lhs-' + i);
1239 red.setGutterMarker(rlf, 'merge', lhs_button.get(0));
1240 }
1241 }
1242 });
1243 this.trace('change', 'markup rhs-editor time', timer.stop());
1244
1245 // mark text deleted, LCS changes
1246 var marktext = [], i, j, k, p;
1247 for (i = 0; this.settings.lcs && i < changes.length; ++i) {
1248 var change = changes[i];
1249 var llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0;
1250 var llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0;
1251 var rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0;
1252 var rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0;
1253
1254 if (!this._is_change_in_view(vp, change)) {
1255 // if the change is outside the viewport, skip
1256 continue;
1257 }
1258 if (change['op'] == 'd') {
1259 // apply delete to cross-out (left-hand side only)
1260 var from = llf;
1261 var to = llt;
1262 var to_ln = led.lineInfo(to);
1263 if (to_ln) {
1264 marktext.push([led, {line:from, ch:0}, {line:to, ch:to_ln.text.length}, {className: 'mergely ch d lhs'}]);
1265 }
1266 }
1267 else if (change['op'] == 'c') {
1268 // apply LCS changes to each line
1269 for (j = llf, k = rlf, p = 0;
1270 ((j >= 0) && (j <= llt)) || ((k >= 0) && (k <= rlt));
1271 ++j, ++k) {
1272 var lhs_line, rhs_line;
1273 if (k + p > rlt) {
1274 // lhs continues past rhs, mark lhs as deleted
1275 lhs_line = led.getLine( j );
1276 marktext.push([led, {line:j, ch:0}, {line:j, ch:lhs_line.length}, {className: 'mergely ch d lhs'}]);
1277 continue;
1278 }
1279 if (j + p > llt) {
1280 // rhs continues past lhs, mark rhs as added
1281 rhs_line = red.getLine( k );
1282 marktext.push([red, {line:k, ch:0}, {line:k, ch:rhs_line.length}, {className: 'mergely ch a rhs'}]);
1283 continue;
1284 }
1285 lhs_line = led.getLine( j );
1286 rhs_line = red.getLine( k );
1287 var lcs = new Mgly.LCS(lhs_line, rhs_line);
1288 lcs.diff(
1289 function added (from, to) {
1290 marktext.push([red, {line:k, ch:from}, {line:k, ch:to}, {className: 'mergely ch a rhs'}]);
1291 },
1292 function removed (from, to) {
1293 marktext.push([led, {line:j, ch:from}, {line:j, ch:to}, {className: 'mergely ch d lhs'}]);
1294 }
1295 );
1296 }
1297 }
1298 }
1299 this.trace('change', 'LCS marktext time', timer.stop());
1300
1301 // mark changes outside closure
1302 led.operation(function() {
1303 // apply lhs markup
1304 for (var i = 0; i < marktext.length; ++i) {
1305 var m = marktext[i];
1306 if (m[0].doc.id != led.getDoc().id) continue;
1307 self.chfns[self.id + '-lhs'].push(m[0].markText(m[1], m[2], m[3]));
1308 }
1309 });
1310 red.operation(function() {
1311 // apply lhs markup
1312 for (var i = 0; i < marktext.length; ++i) {
1313 var m = marktext[i];
1314 if (m[0].doc.id != red.getDoc().id) continue;
1315 self.chfns[self.id + '-rhs'].push(m[0].markText(m[1], m[2], m[3]));
1316 }
1317 });
1318
1319 this.trace('change', 'LCS markup time', timer.stop());
1320
1321 // merge buttons
1322 var ed = {lhs:led, rhs:red};
1323 this.element.find('.merge-button').on('click', function(ev){
1324 // side of mouseenter
1325 var side = 'rhs';
1326 var oside = 'lhs';
1327 var parent = jQuery(this).parents('#' + self.id + '-editor-lhs');
1328 if (parent.length) {
1329 side = 'lhs';
1330 oside = 'rhs';
1331 }
1332 var pos = ed[side].coordsChar({left:ev.pageX, top:ev.pageY});
1333
1334 // get the change id
1335 var cid = null;
1336 var info = ed[side].lineInfo(pos.line);
1337 jQuery.each(info.bgClass.split(' '), function(i, clazz) {
1338 if (clazz.indexOf('cid-') == 0) {
1339 cid = parseInt(clazz.split('-')[1], 10);
1340 return false;
1341 }
1342 });
1343 var change = self.changes[cid];
1344 self._merge_change(change, side, oside);
1345 return false;
1346 });
1347
1348 // gutter markup
1349 var lhsLineNumbers = $('#mergely-lhs ~ .CodeMirror').find('.CodeMirror-linenumber');
1350 var rhsLineNumbers = $('#mergely-rhs ~ .CodeMirror').find('.CodeMirror-linenumber');
1351 rhsLineNumbers.removeClass('mergely current');
1352 lhsLineNumbers.removeClass('mergely current');
1353 for (var i = 0; i < changes.length; ++i) {
1354 if (current_diff == i && change.op !== 'd') {
1355 var change = changes[i];
1356 var j, jf = change['rhs-line-from'], jt = change['rhs-line-to'] + 1;
1357 for (j = jf; j < jt; j++) {
1358 var n = (j + 1).toString();
1359 rhsLineNumbers
1360 .filter(function(i, node) { return $(node).text() === n; })
1361 .addClass('mergely current');
1362 }
1363 }
1364 if (current_diff == i && change.op !== 'a') {
1365 var change = changes[i];
1366 jf = change['lhs-line-from'], jt = change['lhs-line-to'] + 1;
1367 for (j = jf; j < jt; j++) {
1368 var n = (j + 1).toString();
1369 lhsLineNumbers
1370 .filter(function(i, node) { return $(node).text() === n; })
1371 .addClass('mergely current');
1372 }
1373 }
1374 }
1375
1376 this.trace('change', 'markup buttons time', timer.stop());
1377 },
1378 _merge_change : function(change, side, oside) {
1379 if (!change) return;
1380 var led = this.editor[this.id+'-lhs'];
1381 var red = this.editor[this.id+'-rhs'];
1382 var ed = {lhs:led, rhs:red};
1383 var i, from, to;
1384
1385 var text = ed[side].getRange(
1386 CodeMirror.Pos(change[side + '-line-from'], 0),
1387 CodeMirror.Pos(change[side + '-line-to'] + 1, 0));
1388
1389 if (change['op'] == 'c') {
1390 ed[oside].replaceRange(text,
1391 CodeMirror.Pos(change[oside + '-line-from'], 0),
1392 CodeMirror.Pos(change[oside + '-line-to'] + 1, 0));
1393 }
1394 else if (side == 'rhs') {
1395 if (change['op'] == 'a') {
1396 ed[oside].replaceRange(text,
1397 CodeMirror.Pos(change[oside + '-line-from'] + 1, 0),
1398 CodeMirror.Pos(change[oside + '-line-to'] + 1, 0));
1399 }
1400 else {// 'd'
1401 from = parseInt(change[oside + '-line-from'], 10);
1402 to = parseInt(change[oside + '-line-to'], 10);
1403 for (i = to; i >= from; --i) {
1404 ed[oside].setCursor({line: i, ch: -1});
1405 ed[oside].execCommand('deleteLine');
1406 }
1407 }
1408 }
1409 else if (side == 'lhs') {
1410 if (change['op'] == 'a') {
1411 from = parseInt(change[oside + '-line-from'], 10);
1412 to = parseInt(change[oside + '-line-to'], 10);
1413 for (i = to; i >= from; --i) {
1414 //ed[oside].removeLine(i);
1415 ed[oside].setCursor({line: i, ch: -1});
1416 ed[oside].execCommand('deleteLine');
1417 }
1418 }
1419 else {// 'd'
1420 ed[oside].replaceRange( text,
1421 CodeMirror.Pos(change[oside + '-line-from'] + 1, 0));
1422 }
1423 }
1424 //reset
1425 ed['lhs'].setValue(ed['lhs'].getValue());
1426 ed['rhs'].setValue(ed['rhs'].getValue());
1427
1428 this._scroll_to_change(change);
1429 },
1430 _draw_info: function(editor_name1, editor_name2) {
1431 var visible_page_height = jQuery(this.editor[editor_name1].getScrollerElement()).height();
1432 var gutter_height = jQuery(this.editor[editor_name1].getScrollerElement()).children(':first-child').height();
1433 var dcanvas = document.getElementById(editor_name1 + '-' + editor_name2 + '-canvas');
1434 if (dcanvas == undefined) throw 'Failed to find: ' + editor_name1 + '-' + editor_name2 + '-canvas';
1435 var clhs = this.element.find('#' + this.id + '-lhs-margin');
1436 var crhs = this.element.find('#' + this.id + '-rhs-margin');
1437 return {
1438 visible_page_height: visible_page_height,
1439 gutter_height: gutter_height,
1440 visible_page_ratio: (visible_page_height / gutter_height),
1441 margin_ratio: (visible_page_height / gutter_height),
1442 lhs_scroller: jQuery(this.editor[editor_name1].getScrollerElement()),
1443 rhs_scroller: jQuery(this.editor[editor_name2].getScrollerElement()),
1444 lhs_lines: this.editor[editor_name1].lineCount(),
1445 rhs_lines: this.editor[editor_name2].lineCount(),
1446 dcanvas: dcanvas,
1447 clhs: clhs,
1448 crhs: crhs,
1449 lhs_xyoffset: jQuery(clhs).offset(),
1450 rhs_xyoffset: jQuery(crhs).offset()
1451 };
1452 },
1453 _draw_diff: function(editor_name1, editor_name2, changes) {
1454 var ex = this._draw_info(editor_name1, editor_name2);
1455 var mcanvas_lhs = ex.clhs.get(0);
1456 var mcanvas_rhs = ex.crhs.get(0);
1457 var ctx = ex.dcanvas.getContext('2d');
1458 var ctx_lhs = mcanvas_lhs.getContext('2d');
1459 var ctx_rhs = mcanvas_rhs.getContext('2d');
1460
1461 this.trace('draw', 'visible_page_height', ex.visible_page_height);
1462 this.trace('draw', 'gutter_height', ex.gutter_height);
1463 this.trace('draw', 'visible_page_ratio', ex.visible_page_ratio);
1464 this.trace('draw', 'lhs-scroller-top', ex.lhs_scroller.scrollTop());
1465 this.trace('draw', 'rhs-scroller-top', ex.rhs_scroller.scrollTop());
1466
1467 jQuery.each(this.element.find('canvas'), function () {
1468 jQuery(this).get(0).height = ex.visible_page_height;
1469 });
1470
1471 ex.clhs.unbind('click');
1472 ex.crhs.unbind('click');
1473
1474 ctx_lhs.beginPath();
1475 ctx_lhs.fillStyle = this.settings.bgcolor;
1476 ctx_lhs.strokeStyle = '#888';
1477 ctx_lhs.fillRect(0, 0, 6.5, ex.visible_page_height);
1478 ctx_lhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
1479
1480 ctx_rhs.beginPath();
1481 ctx_rhs.fillStyle = this.settings.bgcolor;
1482 ctx_rhs.strokeStyle = '#888';
1483 ctx_rhs.fillRect(0, 0, 6.5, ex.visible_page_height);
1484 ctx_rhs.strokeRect(0, 0, 6.5, ex.visible_page_height);
1485
1486 var vp = this._get_viewport(editor_name1, editor_name2);
1487 for (var i = 0; i < changes.length; ++i) {
1488 var change = changes[i];
1489 var fill = this.settings.fgcolor[change['op']];
1490 if (this._current_diff==i) {
1491 fill = '#000';
1492 }
1493
1494 this.trace('draw', change);
1495 // margin indicators
1496 var lhs_y_start = ((change['lhs-y-start'] + ex.lhs_scroller.scrollTop()) * ex.visible_page_ratio);
1497 var lhs_y_end = ((change['lhs-y-end'] + ex.lhs_scroller.scrollTop()) * ex.visible_page_ratio) + 1;
1498 var rhs_y_start = ((change['rhs-y-start'] + ex.rhs_scroller.scrollTop()) * ex.visible_page_ratio);
1499 var rhs_y_end = ((change['rhs-y-end'] + ex.rhs_scroller.scrollTop()) * ex.visible_page_ratio) + 1;
1500 this.trace('draw', 'marker calculated', lhs_y_start, lhs_y_end, rhs_y_start, rhs_y_end);
1501
1502 ctx_lhs.beginPath();
1503 ctx_lhs.fillStyle = fill;
1504 ctx_lhs.strokeStyle = '#000';
1505 ctx_lhs.lineWidth = 0.5;
1506 ctx_lhs.fillRect(1.5, lhs_y_start, 4.5, Math.max(lhs_y_end - lhs_y_start, 5));
1507 ctx_lhs.strokeRect(1.5, lhs_y_start, 4.5, Math.max(lhs_y_end - lhs_y_start, 5));
1508
1509 ctx_rhs.beginPath();
1510 ctx_rhs.fillStyle = fill;
1511 ctx_rhs.strokeStyle = '#000';
1512 ctx_rhs.lineWidth = 0.5;
1513 ctx_rhs.fillRect(1.5, rhs_y_start, 4.5, Math.max(rhs_y_end - rhs_y_start, 5));
1514 ctx_rhs.strokeRect(1.5, rhs_y_start, 4.5, Math.max(rhs_y_end - rhs_y_start, 5));
1515
1516 if (!this._is_change_in_view(vp, change)) {
1517 continue;
1518 }
1519
1520 lhs_y_start = change['lhs-y-start'];
1521 lhs_y_end = change['lhs-y-end'];
1522 rhs_y_start = change['rhs-y-start'];
1523 rhs_y_end = change['rhs-y-end'];
1524
1525 var radius = 3;
1526
1527 // draw left box
1528 ctx.beginPath();
1529 ctx.strokeStyle = fill;
1530 ctx.lineWidth = (this._current_diff==i) ? 1.5 : 1;
1531
1532 var rectWidth = this.draw_lhs_width;
1533 var rectHeight = lhs_y_end - lhs_y_start - 1;
1534 var rectX = this.draw_lhs_min;
1535 var rectY = lhs_y_start;
1536 // top and top top-right corner
1537
1538 // draw left box
1539 ctx.moveTo(rectX, rectY);
1540 if (navigator.appName == 'Microsoft Internet Explorer') {
1541 // IE arcs look awful
1542 ctx.lineTo(this.draw_lhs_min + this.draw_lhs_width, lhs_y_start);
1543 ctx.lineTo(this.draw_lhs_min + this.draw_lhs_width, lhs_y_end + 1);
1544 ctx.lineTo(this.draw_lhs_min, lhs_y_end + 1);
1545 }
1546 else {
1547 if (rectHeight <= 0) {
1548 ctx.lineTo(rectX + rectWidth, rectY);
1549 }
1550 else {
1551 ctx.arcTo(rectX + rectWidth, rectY, rectX + rectWidth, rectY + radius, radius);
1552 ctx.arcTo(rectX + rectWidth, rectY + rectHeight, rectX + rectWidth - radius, rectY + rectHeight, radius);
1553 }
1554 // bottom line
1555 ctx.lineTo(rectX, rectY + rectHeight);
1556 }
1557 ctx.stroke();
1558
1559 rectWidth = this.draw_rhs_width;
1560 rectHeight = rhs_y_end - rhs_y_start - 1;
1561 rectX = this.draw_rhs_max;
1562 rectY = rhs_y_start;
1563
1564 // draw right box
1565 ctx.moveTo(rectX, rectY);
1566 if (navigator.appName == 'Microsoft Internet Explorer') {
1567 ctx.lineTo(this.draw_rhs_max - this.draw_rhs_width, rhs_y_start);
1568 ctx.lineTo(this.draw_rhs_max - this.draw_rhs_width, rhs_y_end + 1);
1569 ctx.lineTo(this.draw_rhs_max, rhs_y_end + 1);
1570 }
1571 else {
1572 if (rectHeight <= 0) {
1573 ctx.lineTo(rectX - rectWidth, rectY);
1574 }
1575 else {
1576 ctx.arcTo(rectX - rectWidth, rectY, rectX - rectWidth, rectY + radius, radius);
1577 ctx.arcTo(rectX - rectWidth, rectY + rectHeight, rectX - radius, rectY + rectHeight, radius);
1578 }
1579 ctx.lineTo(rectX, rectY + rectHeight);
1580 }
1581 ctx.stroke();
1582
1583 // connect boxes
1584 var cx = this.draw_lhs_min + this.draw_lhs_width;
1585 var cy = lhs_y_start + (lhs_y_end + 1 - lhs_y_start) / 2.0;
1586 var dx = this.draw_rhs_max - this.draw_rhs_width;
1587 var dy = rhs_y_start + (rhs_y_end + 1 - rhs_y_start) / 2.0;
1588 ctx.moveTo(cx, cy);
1589 if (cy == dy) {
1590 ctx.lineTo(dx, dy);
1591 }
1592 else {
1593 // fancy!
1594 ctx.bezierCurveTo(
1595 cx + 12, cy - 3, // control-1 X,Y
1596 dx - 12, dy - 3, // control-2 X,Y
1597 dx, dy);
1598 }
1599 ctx.stroke();
1600 }
1601
1602 // visible window feedback
1603 ctx_lhs.fillStyle = this.settings.vpcolor;
1604 ctx_rhs.fillStyle = this.settings.vpcolor;
1605
1606 var lto = ex.clhs.height() * ex.visible_page_ratio;
1607 var lfrom = (ex.lhs_scroller.scrollTop() / ex.gutter_height) * ex.clhs.height();
1608 var rto = ex.crhs.height() * ex.visible_page_ratio;
1609 var rfrom = (ex.rhs_scroller.scrollTop() / ex.gutter_height) * ex.crhs.height();
1610 this.trace('draw', 'cls.height', ex.clhs.height());
1611 this.trace('draw', 'lhs_scroller.scrollTop()', ex.lhs_scroller.scrollTop());
1612 this.trace('draw', 'gutter_height', ex.gutter_height);
1613 this.trace('draw', 'visible_page_ratio', ex.visible_page_ratio);
1614 this.trace('draw', 'lhs from', lfrom, 'lhs to', lto);
1615 this.trace('draw', 'rhs from', rfrom, 'rhs to', rto);
1616
1617 ctx_lhs.fillRect(1.5, lfrom, 4.5, lto);
1618 ctx_rhs.fillRect(1.5, rfrom, 4.5, rto);
1619
1620 ex.clhs.click(function (ev) {
1621 var y = ev.pageY - ex.lhs_xyoffset.top - (lto / 2);
1622 var sto = Math.max(0, (y / mcanvas_lhs.height) * ex.lhs_scroller.get(0).scrollHeight);
1623 ex.lhs_scroller.scrollTop(sto);
1624 });
1625 ex.crhs.click(function (ev) {
1626 var y = ev.pageY - ex.rhs_xyoffset.top - (rto / 2);
1627 var sto = Math.max(0, (y / mcanvas_rhs.height) * ex.rhs_scroller.get(0).scrollHeight);
1628 ex.rhs_scroller.scrollTop(sto);
1629 });
1630 },
1631 trace: function(name) {
1632 if(this.settings._debug.indexOf(name) >= 0) {
1633 arguments[0] = name + ':';
1634 console.log([].slice.apply(arguments));
1635 }
1636 }
1637 });
1638
1639 jQuery.pluginMaker = function(plugin) {
1640 // add the plugin function as a jQuery plugin
1641 jQuery.fn[plugin.prototype.name] = function(options) {
1642 // get the arguments
1643 var args = jQuery.makeArray(arguments),
1644 after = args.slice(1);
1645 var rc;
1646 this.each(function() {
1647 // see if we have an instance
1648 var instance = jQuery.data(this, plugin.prototype.name);
1649 if (instance) {
1650 // call a method on the instance
1651 if (typeof options == "string") {
1652 rc = instance[options].apply(instance, after);
1653 } else if (instance.update) {
1654 // call update on the instance
1655 return instance.update.apply(instance, args);
1656 }
1657 } else {
1658 // create the plugin
1659 var _plugin = new plugin(this, options);
1660 }
1661 });
1662 if (rc != undefined) return rc;
1663 };
1664 };
1665
1666 // make the mergely widget
1667 jQuery.pluginMaker(Mgly.mergely);
1668
1669 })( window, document, jQuery, CodeMirror ); No newline at end of file
@@ -1,225 +0,0 b''
1 ## -*- coding: utf-8 -*-
2
3 <%inherit file="/base/base.html"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
5
6 <%def name="js_extra()">
7 <script type="text/javascript" src="${h.asset('js/mergerly.js')}"></script>
8 </%def>
9
10 <%def name="css_extra()">
11 <link rel="stylesheet" type="text/css" href="${h.asset('css/mergerly.css')}"/>
12 </%def>
13
14 <%def name="title()">
15 ${_('%s File side-by-side diff') % c.repo_name}
16 %if c.rhodecode_name:
17 &middot; ${h.branding(c.rhodecode_name)}
18 %endif
19 </%def>
20
21 <%def name="breadcrumbs_links()">
22 r${c.commit_1.revision}:${h.short_id(c.commit_1.raw_id)} ... r${c.commit_2.revision}:${h.short_id(c.commit_2.raw_id)}
23 </%def>
24
25 <%def name="menu_bar_nav()">
26 ${self.menu_items(active='repositories')}
27 </%def>
28
29 <%def name="menu_bar_subnav()">
30 ${self.repo_menu(active='changelog')}
31 </%def>
32
33 <%def name="main()">
34 <div class="box">
35 <div class="title">
36 ${self.repo_page_title(c.rhodecode_db_repo)}
37 </div>
38
39 <div class="breadcrumbs">
40 ${_('Side-by-side Diff')} r${c.commit_1.revision}:${h.short_id(c.commit_1.raw_id)} ... r${c.commit_2.revision}:${h.short_id(c.commit_2.raw_id)}
41 </div>
42 <div class="cs_files">
43 <table class="compare_view_files commit_diff">
44 <tr class="cs_${c.diff_data['operation']} collapse_file" fid="${c.FID}">
45 <td class="cs_icon_td">
46 <span class="collapse_file_icon" fid="${c.FID}"></span>
47 </td>
48 <td class="cs_icon_td">
49 <div class="flag_status not_reviewed hidden"></div>
50 </td>
51 <td class="cs_${c.diff_data['operation']}" id="a_${c.FID}">
52 <div class="node">
53 <a href="#a_${c.FID}">
54 <i class="icon-file-${c.diff_data['operation'].lower()}"></i>
55 ${h.safe_unicode(c.node1.path)}
56 </a>
57 </div>
58 </td>
59 <td>
60 <div class="changes pull-right">${h.fancy_file_stats(c.diff_data['stats'])}</div>
61 <div class="comment-bubble pull-right" data-path="${h.safe_unicode(c.node1.path)}">
62 <i class="icon-comment"></i>
63 </div>
64 </td>
65 </tr>
66 <tr fid="${c.FID}" id="diff_${c.FID}" class="diff_links">
67 <td></td>
68 <td></td>
69 <td class="cs_${c.diff_data['operation']}">
70 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(c.node1.path), c.commit_1.raw_id, c.commit_2.raw_id, c.diff_data['operation'])}
71 </td>
72 <td class="td-actions rc-form">
73 <div id="ignorews" class="btn-link show-inline-comments">
74 <span data-enabled=false class="toggle">${_('Ignore whitespace')}</span>
75 <span data-enabled=true class="toggle" style="display: none">${_('Show whitespace')}</span>
76 </div> |
77
78 <div id="edit_mode" class="btn-link show-inline-comments">
79 <span data-enabled=true class="toggle">${_('Enable editor mode')}</span>
80 <span data-enabled=false class="toggle" style="display: none">${_('Disable editor mode')}</span>
81 </div> |
82
83 <div class="btn-link show-inline-comments">
84 <span id="prev_change" title="${_('Previous change')}"><i class="icon-left"></i></span>
85 <span id="next_change" title="${_('Next change')}"><i class="icon-right"></i></span>
86 </div>
87
88 </td>
89 </tr>
90 <tr id="tr_${c.FID}">
91 <td></td>
92 <td></td>
93 <td class="injected_diff" colspan="2">
94 <div class="diff-container" id="${'diff-container-%s' % (id(c.diff_data['operation']))}">
95 <div id="${c.FID}" class="diffblock margined comm">
96 <div class="diff-container" >
97 <div class="diffblock comm sidebyside">
98 <div class="code-header">
99 <div class="changeset_header">
100 ${_('mode')}: <span id="selected_mode">plain</span> |
101 </div>
102 </div>
103 <div id="compare"></div>
104 </div>
105 </div>
106 </div>
107 </div>
108 </td>
109 </tr>
110 </table>
111 </div>
112
113
114 <script>
115 var orig1_url = '${h.url('files_raw_home',repo_name=c.repo_name,f_path=h.safe_unicode(c.node1.path),revision=c.commit_1.raw_id)}';
116 var orig2_url = '${h.url('files_raw_home',repo_name=c.repo_name,f_path=h.safe_unicode(c.node2.path),revision=c.commit_2.raw_id)}';
117 $(document).ready(function () {
118
119
120 var editor = $('#compare');
121 editor.mergely({
122 autoupdate: true,
123 width: 'auto',
124 height: '600',
125 fgcolor: {a: '#ddffdd', c: '#cccccc', d: '#ffdddd'},
126 bgcolor: '#fff',
127 viewport: false,
128 cmsettings: {
129 mode: 'text/plain',
130 readOnly: true,
131 lineWrapping: false,
132 lineNumbers: true
133 }
134 });
135
136
137 var lhs = function(deferred) {
138 if ("${c.node1.is_binary}" == "True") {
139 deferred.resolve('Binary file');
140 }
141 else if ("${c.node1.commit.__class__.__name__}" == "EmptyCommit") {
142 deferred.resolve('');
143 }
144 else {
145 editor.mergely('lhs', 'loading diff...');
146 $.ajax(orig1_url, {
147 dataType: 'text',
148 success: function(data) {
149 // call the complete function to let CodeMirror know
150 deferred.resolve(data);
151 }
152 });
153 }
154 };
155
156 var rhs = function(deferred) {
157 if ("${c.node2.is_binary}" == "True") {
158 deferred.resolve('Binary file');
159 }
160 else if ("${c.node2.commit.__class__.__name__}" == "EmptyCommit") {
161 deferred.resolve('');
162 }
163 else {
164 editor.mergely('rhs', 'loading diff...');
165 $.ajax(orig2_url, {
166 dataType: 'text',
167 success: function(data) {
168 // call the complete function to let CodeMirror know
169 deferred.resolve(data);
170 }
171 });
172 }
173 };
174
175 var deferred_lhs = $.Deferred();
176 var deferred_rhs = $.Deferred();
177 $.when(
178 deferred_lhs, deferred_rhs
179 ).done(function(lhs_response, rhs_response) {
180 editor.mergely('lhs', lhs_response);
181 editor.mergely('rhs', rhs_response);
182
183 var detected_mode = detectCodeMirrorModeFromExt(
184 '${h.safe_unicode(c.node1.path.split("/")[-1])}', true);
185 if (detected_mode) {
186 setCodeMirrorMode(editor.mergely('cm', 'lhs'), detected_mode);
187 setCodeMirrorMode(editor.mergely('cm', 'rhs'), detected_mode);
188 $('#selected_mode').html(detected_mode);
189 }
190 });
191 // load via ajax, and use deferred signals to notify when finished.
192 lhs(deferred_lhs);
193 rhs(deferred_rhs);
194
195 $("#ignorews").click(function() {
196 $("#ignorews .toggle").toggle();
197 var val = $('#ignorews .toggle:visible').data()['enabled'];
198 editor.mergely('options', {ignorews: val});
199 editor.mergely('update');
200 });
201
202 $("#edit_mode").click(function() {
203 $("#edit_mode .toggle").toggle();
204 var val = $('#edit_mode .toggle:visible').data()['enabled'];
205 editor.mergely('cm', 'lhs').setOption('readOnly', val);
206 editor.mergely('cm', 'rhs').setOption('readOnly', val);
207 editor.mergely('update');
208 });
209
210 $('#prev_change').on('click', function() {
211 editor.mergely('scrollToDiff', 'prev');
212 });
213 $('#next_change').on('click', function() {
214 editor.mergely('scrollToDiff', 'next');
215 });
216
217 // extend content dynamically on this component for readability
218 $('#content').css({'max-width': '2000px'});
219 editor.mergely('resize');
220
221 });
222 </script>
223
224 </div>
225 </%def>
@@ -1,159 +0,0 b''
1 <%inherit file="/base/base.html"/>
2 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
3 <%def name="title()">
4 ${_('%s File Diff') % c.repo_name}
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
8 </%def>
9
10 <%def name="breadcrumbs_links()">
11 ${_('Compare')}
12 r${c.commit_1.revision}:${h.short_id(c.commit_1.raw_id)}
13 % if c.filename1 != c.filename:
14 <i class="icon-file"></i> ${c.filename1}
15 % endif
16 ...
17 r${c.commit_2.revision}:${h.short_id(c.commit_2.raw_id)}
18 </%def>
19
20 <%def name="menu_bar_nav()">
21 ${self.menu_items(active='repositories')}
22 </%def>
23
24 <%def name="menu_bar_subnav()">
25 ${self.repo_menu(active='changelog')}
26 </%def>
27
28 <%def name="breadcrumbs_links()">
29 ${_('Compare')}
30 r${c.commit_1.revision}:${h.short_id(c.commit_1.raw_id)}
31 % if c.filename1 != c.filename:
32 <i class="icon-file"></i> ${c.filename1}
33 % endif
34 ...
35 r${c.commit_2.revision}:${h.short_id(c.commit_2.raw_id)}
36 % if c.filename1 == c.filename:
37 ${_('for')} <i class="icon-file"></i> ${c.filename1}
38 % endif
39 </%def>
40
41 <%def name="main()">
42 <div class="box">
43
44 <div class="title">
45 ${self.repo_page_title(c.rhodecode_db_repo)}
46 </div>
47
48 ${self.breadcrumbs()}
49
50 <div class="compare-header">
51
52 %if not c.commit_ranges:
53 <p class="empty_data">${_('No commits')}</p>
54
55 %else:
56 <div class="compare-label">${_('Target')}</div>
57 <div class="compare-value">
58 <code>
59 ${h.link_to('r%s:%s' % (c.commit_1.revision, h.short_id(c.commit_1.raw_id)), h.url('changeset_home',repo_name=c.repo_name, revision=c.commit_1.raw_id))}
60 </code>
61 </div>
62 <div class="compare-label">${_('Source')}</div>
63 <div class="compare-value">
64 <code>
65 ${h.link_to('r%s:%s' % (c.commit_2.revision, h.short_id(c.commit_2.raw_id)), h.url('changeset_home',repo_name=c.repo_name, revision=c.commit_2.raw_id))}
66 </code>
67 </div>
68 %endif
69 </div>
70
71 ##CS
72 <%include file="../compare/compare_commits.html" />
73
74 ## FILES
75 <div class="cs_files_title">
76 <span class="cs_files_expand">
77 <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
78 </span>
79 <h2>
80 % if c.binary_file:
81 ${_('Cannot diff binary files')}
82 % elif (c.lines_added == 0 and c.lines_deleted == 0):
83 ${_('File was not changed in this commit range')}
84 % else:
85 ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted)}
86 % endif
87 </h2>
88 </div>
89
90 % if (c.lines_added > 0 or c.lines_deleted > 0):
91 <div class="cs_files">
92 <table class="compare_view_files commit_diff">
93 %for FID, (cs1, cs2, change, path, diff, stats, file) in c.changes.iteritems():
94 <tr class="cs_${change} collapse_file" fid="${FID}">
95 <td class="cs_icon_td">
96 <span class="collapse_file_icon" fid="${FID}"></span>
97 </td>
98 <td class="cs_icon_td">
99 <div class="flag_status not_reviewed hidden"></div>
100 </td>
101 <td class="cs_${change}" id="a_${FID}">
102 <div class="node">
103 <a href="#a_${FID}">
104 <i class="icon-file-${change.lower()}"></i>
105 ${h.safe_unicode(path)}
106 </a>
107 </div>
108 </td>
109 <td>
110 %if (stats):
111 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
112 %endif
113 <div class="comment-bubble pull-right" data-path="${path}">
114 <i class="icon-comment"></i>
115 </div>
116 </td>
117 </tr>
118 <tr fid="${FID}" id="diff_${FID}" class="diff_links">
119 <td></td>
120 <td></td>
121 <td class="cs_${change}">
122 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), cs1, cs2, change, file)}
123 </td>
124 <td class="td-actions rc-form">
125 ${c.ignorews_url(request.GET, h.FID(cs2,path))} |
126 ${c.context_url(request.GET, h.FID(cs2,path))} |
127 <div data-comment-id="${h.FID(cs2,path)}" class="btn-link show-inline-comments comments-visible">
128 <span class="comments-show">${_('Show comments')}</span>
129 <span class="comments-hide">${_('Hide comments')}</span>
130 </div>
131 </td>
132 </tr>
133 <tr id="tr_${FID}">
134 <td></td>
135 <td></td>
136 <td class="injected_diff" colspan="2">
137 <div class="diff-container" id="${'diff-container-%s' % (id(change))}">
138 <div id="${FID}" class="diffblock margined comm">
139 <div class="code-body">
140 <div class="full_f_path" path="${h.safe_unicode(path)}"></div>
141 ${diff|n}
142 % if file and file["is_limited_diff"]:
143 % if file["exceeds_limit"]:
144 ${diff_block.file_message()}
145 % else:
146 <h5>${_('Diff was truncated. File content available only in full diff.')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a></h5>
147 % endif
148 % endif
149 </div>
150 </div>
151 </div>
152 </td>
153 </tr>
154 %endfor
155 </table>
156 </div>
157 % endif
158 </div>
159 </%def>
General Comments 0
You need to be logged in to leave comments. Login now