CLASS zcl_abapgit_gui_page_diff DEFINITION PUBLIC FINAL CREATE PUBLIC INHERITING FROM zcl_abapgit_gui_page. PUBLIC SECTION. CONSTANTS: BEGIN OF c_fstate, local TYPE char1 VALUE 'L', remote TYPE char1 VALUE 'R', both TYPE char1 VALUE 'B', END OF c_fstate. TYPES: BEGIN OF ty_file_diff, path TYPE string, filename TYPE string, lstate TYPE char1, rstate TYPE char1, fstate TYPE char1, " FILE state - Abstraction for shorter ifs o_diff TYPE REF TO zcl_abapgit_diff, changed_by TYPE xubname, type TYPE string, END OF ty_file_diff, tt_file_diff TYPE STANDARD TABLE OF ty_file_diff. METHODS: constructor IMPORTING iv_key TYPE zif_abapgit_persistence=>ty_repo-key is_file TYPE zif_abapgit_definitions=>ty_file OPTIONAL is_object TYPE zif_abapgit_definitions=>ty_item OPTIONAL iv_supress_stage TYPE abap_bool DEFAULT abap_false RAISING zcx_abapgit_exception, zif_abapgit_gui_page~on_event REDEFINITION. PROTECTED SECTION. METHODS: render_content REDEFINITION, scripts REDEFINITION. PRIVATE SECTION. CONSTANTS: BEGIN OF c_actions, toggle_unified TYPE string VALUE 'toggle_unified', END OF c_actions. DATA: mt_diff_files TYPE tt_file_diff, mt_delayed_lines TYPE zif_abapgit_definitions=>ty_diffs_tt, mv_unified TYPE abap_bool VALUE abap_true, mv_repo_key TYPE zif_abapgit_persistence=>ty_repo-key, mv_seed TYPE string. " Unique page id to bind JS sessionStorage METHODS render_diff IMPORTING is_diff TYPE ty_file_diff RETURNING VALUE(ro_html) TYPE REF TO zcl_abapgit_html. METHODS render_diff_head IMPORTING is_diff TYPE ty_file_diff RETURNING VALUE(ro_html) TYPE REF TO zcl_abapgit_html. METHODS render_table_head RETURNING VALUE(ro_html) TYPE REF TO zcl_abapgit_html. METHODS render_lines IMPORTING is_diff TYPE ty_file_diff RETURNING VALUE(ro_html) TYPE REF TO zcl_abapgit_html. METHODS render_beacon IMPORTING is_diff_line TYPE zif_abapgit_definitions=>ty_diff is_diff TYPE ty_file_diff RETURNING VALUE(ro_html) TYPE REF TO zcl_abapgit_html. METHODS render_line_split IMPORTING is_diff_line TYPE zif_abapgit_definitions=>ty_diff iv_fstate TYPE char1 RETURNING VALUE(ro_html) TYPE REF TO zcl_abapgit_html. METHODS render_line_unified IMPORTING is_diff_line TYPE zif_abapgit_definitions=>ty_diff OPTIONAL RETURNING VALUE(ro_html) TYPE REF TO zcl_abapgit_html. METHODS append_diff IMPORTING it_remote TYPE zif_abapgit_definitions=>ty_files_tt it_local TYPE zif_abapgit_definitions=>ty_files_item_tt is_status TYPE zif_abapgit_definitions=>ty_result RAISING zcx_abapgit_exception. METHODS build_menu IMPORTING iv_supress_stage TYPE abap_bool RETURNING VALUE(ro_menu) TYPE REF TO zcl_abapgit_html_toolbar. METHODS is_binary IMPORTING iv_d1 TYPE xstring iv_d2 TYPE xstring RETURNING VALUE(rv_yes) TYPE abap_bool. ENDCLASS. CLASS ZCL_ABAPGIT_GUI_PAGE_DIFF IMPLEMENTATION. METHOD append_diff. DATA: lv_offs TYPE i, ls_r_dummy LIKE LINE OF it_remote ##NEEDED, ls_l_dummy LIKE LINE OF it_local ##NEEDED. FIELD-SYMBOLS: LIKE LINE OF it_remote, LIKE LINE OF it_local, LIKE LINE OF mt_diff_files. READ TABLE it_remote ASSIGNING WITH KEY filename = is_status-filename path = is_status-path. IF sy-subrc <> 0. ASSIGN ls_r_dummy TO . ENDIF. READ TABLE it_local ASSIGNING WITH KEY file-filename = is_status-filename file-path = is_status-path. IF sy-subrc <> 0. ASSIGN ls_l_dummy TO . ENDIF. IF IS INITIAL AND IS INITIAL. zcx_abapgit_exception=>raise( |DIFF: file not found { is_status-filename }| ). ENDIF. APPEND INITIAL LINE TO mt_diff_files ASSIGNING . -path = is_status-path. -filename = is_status-filename. -lstate = is_status-lstate. -rstate = is_status-rstate. IF -lstate IS NOT INITIAL AND -rstate IS NOT INITIAL. -fstate = c_fstate-both. ELSEIF -lstate IS NOT INITIAL. -fstate = c_fstate-local. ELSE. "rstate IS NOT INITIAL, lstate = empty. -fstate = c_fstate-remote. ENDIF. " Changed by IF -item-obj_type IS NOT INITIAL. -changed_by = to_lower( zcl_abapgit_objects=>changed_by( -item ) ). ENDIF. " Extension IF -file-filename IS NOT INITIAL. -type = reverse( -file-filename ). ELSE. -type = reverse( -filename ). ENDIF. FIND FIRST OCCURRENCE OF '.' IN -type MATCH OFFSET lv_offs. -type = reverse( substring( val = -type len = lv_offs ) ). IF -type <> 'xml' AND -type <> 'abap'. -type = 'other'. ENDIF. IF -type = 'other' AND is_binary( iv_d1 = -data iv_d2 = -file-data ) = abap_true. -type = 'binary'. ENDIF. " Diff data IF -type <> 'binary'. IF -fstate = c_fstate-remote. " Remote file leading changes CREATE OBJECT -o_diff EXPORTING iv_new = -data iv_old = -file-data. ELSE. " Local leading changes or both were modified CREATE OBJECT -o_diff EXPORTING iv_new = -file-data iv_old = -data. ENDIF. ENDIF. ENDMETHOD. METHOD build_menu. DATA: lo_sub TYPE REF TO zcl_abapgit_html_toolbar, lt_types TYPE string_table, lt_users TYPE string_table. FIELD-SYMBOLS: LIKE LINE OF mt_diff_files, TYPE string. " Get unique LOOP AT mt_diff_files ASSIGNING . APPEND -type TO lt_types. APPEND -changed_by TO lt_users. ENDLOOP. SORT: lt_types, lt_users. DELETE ADJACENT DUPLICATES FROM: lt_types, lt_users. CREATE OBJECT ro_menu. IF iv_supress_stage = abap_false. ro_menu->add( iv_txt = 'Stage' iv_act = |{ zif_abapgit_definitions=>gc_action-go_stage }?{ mv_repo_key }| iv_id = 'stage-button' iv_opt = zif_abapgit_definitions=>gc_html_opt-strong ). ENDIF. IF lines( lt_types ) > 1 OR lines( lt_users ) > 1. CREATE OBJECT lo_sub EXPORTING iv_id = 'diff-filter'. " File types IF lines( lt_types ) > 1. lo_sub->add( iv_txt = 'TYPE' iv_typ = zif_abapgit_definitions=>gc_action_type-separator ). LOOP AT lt_types ASSIGNING . lo_sub->add( iv_txt = iv_typ = zif_abapgit_definitions=>gc_action_type-onclick iv_aux = 'type' iv_chk = abap_true ). ENDLOOP. ENDIF. " Changed by IF lines( lt_users ) > 1. lo_sub->add( iv_txt = 'CHANGED BY' iv_typ = zif_abapgit_definitions=>gc_action_type-separator ). LOOP AT lt_users ASSIGNING . lo_sub->add( iv_txt = iv_typ = zif_abapgit_definitions=>gc_action_type-onclick iv_aux = 'changed-by' iv_chk = abap_true ). ENDLOOP. ENDIF. ro_menu->add( iv_txt = 'Filter' io_sub = lo_sub ) ##NO_TEXT. ENDIF. ro_menu->add( iv_txt = 'Split/Unified view' iv_act = c_actions-toggle_unified ) ##NO_TEXT. ENDMETHOD. METHOD constructor. DATA: lt_remote TYPE zif_abapgit_definitions=>ty_files_tt, lt_local TYPE zif_abapgit_definitions=>ty_files_item_tt, lt_status TYPE zif_abapgit_definitions=>ty_results_tt, lo_repo TYPE REF TO zcl_abapgit_repo_online, lv_ts TYPE timestamp. FIELD-SYMBOLS: LIKE LINE OF lt_status. super->constructor( ). ms_control-page_title = 'DIFF'. mv_unified = zcl_abapgit_persistence_user=>get_instance( )->get_diff_unified( ). mv_repo_key = iv_key. GET TIME STAMP FIELD lv_ts. mv_seed = |diff{ lv_ts }|. " Generate based on time ASSERT is_file IS INITIAL OR is_object IS INITIAL. " just one passed lo_repo ?= zcl_abapgit_repo_srv=>get_instance( )->get( iv_key ). lt_remote = lo_repo->get_files_remote( ). lt_local = lo_repo->get_files_local( ). lt_status = lo_repo->status( ). IF is_file IS NOT INITIAL. " Diff for one file READ TABLE lt_status ASSIGNING WITH KEY path = is_file-path filename = is_file-filename. append_diff( it_remote = lt_remote it_local = lt_local is_status = ). ELSEIF is_object IS NOT INITIAL. " Diff for whole object LOOP AT lt_status ASSIGNING WHERE obj_type = is_object-obj_type AND obj_name = is_object-obj_name AND match IS INITIAL. append_diff( it_remote = lt_remote it_local = lt_local is_status = ). ENDLOOP. ELSE. " Diff for the whole repo LOOP AT lt_status ASSIGNING WHERE match IS INITIAL. append_diff( it_remote = lt_remote it_local = lt_local is_status = ). ENDLOOP. ENDIF. IF lines( mt_diff_files ) = 0. zcx_abapgit_exception=>raise( 'PAGE_DIFF ERROR: No diff files found' ). ENDIF. ms_control-page_menu = build_menu( iv_supress_stage ). ENDMETHOD. METHOD is_binary. DATA: lv_len TYPE i, lv_idx TYPE i, lv_x TYPE x. FIELD-SYMBOLS LIKE iv_d1. IF iv_d1 IS NOT INITIAL. " One of them might be new and so empty ASSIGN iv_d1 TO . ELSE. ASSIGN iv_d2 TO . ENDIF. lv_len = xstrlen( ). IF lv_len = 0. RETURN. ENDIF. IF lv_len > 100. lv_len = 100. ENDIF. " Simple char range test " stackoverflow.com/questions/277521/how-to-identify-the-file-content-as-ascii-or-binary DO lv_len TIMES. " I'm sure there is more efficient way ... lv_idx = sy-index - 1. lv_x = +lv_idx(1). IF NOT ( lv_x BETWEEN 9 AND 13 OR lv_x BETWEEN 32 AND 126 ). rv_yes = abap_true. EXIT. ENDIF. ENDDO. ENDMETHOD. METHOD render_beacon. DATA: lv_beacon TYPE string. CREATE OBJECT ro_html. IF is_diff_line-beacon > 0. READ TABLE is_diff-o_diff->mt_beacons INTO lv_beacon INDEX is_diff_line-beacon. ELSE. lv_beacon = '---'. ENDIF. ro_html->add( '' ). ro_html->add( '' ). ro_html->add( '' ). IF mv_unified = abap_true. ro_html->add( '' ). ro_html->add( |@@ { is_diff_line-new_num } @@ { lv_beacon }| ). ELSE. ro_html->add( |@@ { is_diff_line-new_num } @@ { lv_beacon }| ). ENDIF. ro_html->add( '' ). ro_html->add( '' ). ENDMETHOD. METHOD render_content. DATA: ls_diff_file LIKE LINE OF mt_diff_files, lo_progress TYPE REF TO zcl_abapgit_progress. CREATE OBJECT ro_html. CREATE OBJECT lo_progress EXPORTING iv_total = lines( mt_diff_files ). ro_html->add( |
| ). ro_html->add( zcl_abapgit_gui_chunk_lib=>render_js_error_banner( ) ). LOOP AT mt_diff_files INTO ls_diff_file. lo_progress->show( iv_current = sy-tabix iv_text = |Render Diff - { ls_diff_file-filename }| ). ro_html->add( render_diff( ls_diff_file ) ). ENDLOOP. ro_html->add( '
' ). ENDMETHOD. METHOD render_diff. CREATE OBJECT ro_html. ro_html->add( |
| ). "#EC NOTEXT ro_html->add( render_diff_head( is_diff ) ). " Content IF is_diff-type <> 'binary'. ro_html->add( '
' ). "#EC NOTEXT ro_html->add( '' ). "#EC NOTEXT ro_html->add( render_table_head( ) ). ro_html->add( render_lines( is_diff ) ). ro_html->add( '
' ). "#EC NOTEXT ELSE. ro_html->add( '
' ). "#EC NOTEXT ro_html->add( 'The content seems to be binary.' ). "#EC NOTEXT ro_html->add( 'Cannot display as diff.' ). "#EC NOTEXT ENDIF. ro_html->add( '
' ). "#EC NOTEXT ro_html->add( '
' ). "#EC NOTEXT ENDMETHOD. METHOD render_diff_head. DATA: ls_stats TYPE zif_abapgit_definitions=>ty_count. CREATE OBJECT ro_html. ro_html->add( '
' ). "#EC NOTEXT IF is_diff-type <> 'binary'. ls_stats = is_diff-o_diff->stats( ). IF is_diff-fstate = c_fstate-both. " Merge stats into 'update' if both were changed ls_stats-update = ls_stats-update + ls_stats-insert + ls_stats-delete. CLEAR: ls_stats-insert, ls_stats-delete. ENDIF. ro_html->add( |+ { ls_stats-insert }| ). ro_html->add( |- { ls_stats-delete }| ). ro_html->add( |~ { ls_stats-update }| ). ENDIF. ro_html->add( |{ is_diff-path }{ is_diff-filename }| ). "#EC NOTEXT ro_html->add( zcl_abapgit_gui_chunk_lib=>render_item_state( iv1 = is_diff-lstate iv2 = is_diff-rstate ) ). IF is_diff-fstate = c_fstate-both AND mv_unified = abap_true. ro_html->add( 'Attention: Unified mode' && ' highlighting for MM assumes local file is newer ! ' ). "#EC NOTEXT ENDIF. ro_html->add( |last change by: { is_diff-changed_by }| ). ro_html->add( '
' ). "#EC NOTEXT ENDMETHOD. METHOD render_lines. DATA: lo_highlighter TYPE REF TO zcl_abapgit_syntax_highlighter, lt_diffs TYPE zif_abapgit_definitions=>ty_diffs_tt, lv_insert_nav TYPE abap_bool. FIELD-SYMBOLS LIKE LINE OF lt_diffs. lo_highlighter = zcl_abapgit_syntax_highlighter=>create( is_diff-filename ). CREATE OBJECT ro_html. lt_diffs = is_diff-o_diff->get( ). LOOP AT lt_diffs ASSIGNING . IF -short = abap_false. lv_insert_nav = abap_true. CONTINUE. ENDIF. IF lv_insert_nav = abap_true. " Insert separator line with navigation ro_html->add( render_beacon( is_diff_line = is_diff = is_diff ) ). lv_insert_nav = abap_false. ENDIF. IF lo_highlighter IS BOUND. -new = lo_highlighter->process_line( -new ). -old = lo_highlighter->process_line( -old ). ELSE. -new = escape( val = -new format = cl_abap_format=>e_html_attr ). -old = escape( val = -old format = cl_abap_format=>e_html_attr ). ENDIF. CONDENSE -new_num. "get rid of leading spaces CONDENSE -old_num. IF mv_unified = abap_true. ro_html->add( render_line_unified( is_diff_line = ) ). ELSE. ro_html->add( render_line_split( is_diff_line = iv_fstate = is_diff-fstate ) ). ENDIF. ENDLOOP. IF mv_unified = abap_true. ro_html->add( render_line_unified( ) ). " Release delayed lines ENDIF. ENDMETHOD. METHOD render_line_split. DATA: lv_new TYPE string, lv_old TYPE string, lv_mark TYPE string, lv_bg TYPE string. CREATE OBJECT ro_html. " New line lv_mark = ` `. IF iv_fstate = c_fstate-both OR is_diff_line-result = zif_abapgit_definitions=>c_diff-update. lv_bg = ' diff_upd'. lv_mark = `~`. ELSEIF is_diff_line-result = zif_abapgit_definitions=>c_diff-insert. lv_bg = ' diff_ins'. lv_mark = `+`. ENDIF. lv_new = || && |{ lv_mark }{ is_diff_line-new }|. " Old line CLEAR lv_bg. lv_mark = ` `. IF iv_fstate = c_fstate-both OR is_diff_line-result = zif_abapgit_definitions=>c_diff-update. lv_bg = ' diff_upd'. lv_mark = `~`. ELSEIF is_diff_line-result = zif_abapgit_definitions=>c_diff-delete. lv_bg = ' diff_del'. lv_mark = `-`. ENDIF. lv_old = || && |{ lv_mark }{ is_diff_line-old }|. " render line, inverse sides if remote is newer ro_html->add( '' ). "#EC NOTEXT IF iv_fstate = c_fstate-remote. " Remote file leading changes ro_html->add( lv_old ). " local ro_html->add( lv_new ). " remote ELSE. " Local leading changes or both were modified ro_html->add( lv_new ). " local ro_html->add( lv_old ). " remote ENDIF. ro_html->add( '' ). "#EC NOTEXT ENDMETHOD. METHOD render_line_unified. FIELD-SYMBOLS LIKE LINE OF mt_delayed_lines. CREATE OBJECT ro_html. " Release delayed subsequent update lines IF is_diff_line-result <> zif_abapgit_definitions=>c_diff-update. LOOP AT mt_delayed_lines ASSIGNING . ro_html->add( '' ). "#EC NOTEXT ro_html->add( || && || && |-{ -old }| ). ro_html->add( '' ). "#EC NOTEXT ENDLOOP. LOOP AT mt_delayed_lines ASSIGNING . ro_html->add( '' ). "#EC NOTEXT ro_html->add( || && || && |+{ -new }| ). ro_html->add( '' ). "#EC NOTEXT ENDLOOP. CLEAR mt_delayed_lines. ENDIF. ro_html->add( '' ). "#EC NOTEXT CASE is_diff_line-result. WHEN zif_abapgit_definitions=>c_diff-update. APPEND is_diff_line TO mt_delayed_lines. " Delay output of subsequent updates WHEN zif_abapgit_definitions=>c_diff-insert. ro_html->add( || && || && |+{ is_diff_line-new }| ). WHEN zif_abapgit_definitions=>c_diff-delete. ro_html->add( || && || && |-{ is_diff_line-old }| ). WHEN OTHERS. "none ro_html->add( || && || && | { is_diff_line-old }| ). ENDCASE. ro_html->add( '' ). "#EC NOTEXT ENDMETHOD. METHOD render_table_head. CREATE OBJECT ro_html. ro_html->add( '' ). "#EC NOTEXT ro_html->add( '' ). "#EC NOTEXT IF mv_unified = abap_true. ro_html->add( 'old' ). "#EC NOTEXT ro_html->add( 'new' ). "#EC NOTEXT ro_html->add( 'code' ). "#EC NOTEXT ELSE. ro_html->add( '' ). "#EC NOTEXT ro_html->add( 'LOCAL' ). "#EC NOTEXT ro_html->add( '' ). "#EC NOTEXT ro_html->add( 'REMOTE' ). "#EC NOTEXT ENDIF. ro_html->add( '' ). "#EC NOTEXT ro_html->add( '' ). "#EC NOTEXT ENDMETHOD. METHOD scripts. ro_html = super->scripts( ). ro_html->add( 'var gHelper = new DiffHelper({' ). ro_html->add( | seed: "{ mv_seed }",| ). ro_html->add( | stageAction: "{ zif_abapgit_definitions=>gc_action-go_stage }",| ). ro_html->add( ' ids: {' ). ro_html->add( ' diffList: "diff-list",' ). ro_html->add( ' filterMenu: "diff-filter",' ). ro_html->add( ' stageButton: "stage-button"' ). ro_html->add( ' }' ). ro_html->add( '});' ). ENDMETHOD. METHOD zif_abapgit_gui_page~on_event. CASE iv_action. WHEN c_actions-toggle_unified. " Toggle file diplay mv_unified = zcl_abapgit_persistence_user=>get_instance( )->toggle_diff_unified( ). ev_state = zif_abapgit_definitions=>gc_event_state-re_render. ENDCASE. ENDMETHOD. ENDCLASS.