Post

Integrating GitLab with ABAP for File Management

Integrating GitLab with ABAP for File Management

GitLab provides a convenient API to interact with Git repositories without needing Git tools installed locally. This is particularly useful when accessing a Git repository directly from ABAP code.

For our Weblate integration, we required the ability to transfer translation files to and from GitLab. To achieve this, we implemented several GitLab API endpoints within an ABAP class.


⚙️ Prerequisites

Before interacting with the GitLab API through HTTP, you may need to configure the GitLab server using transaction SM59. This allows you to avoid hardcoding server details in your ABAP code. We opted to work with multiple servers by using hostnames directly, without relying on SM59.

To access the GitLab API anonymously, you must set up a Personal Access Token for use in the HTTP calls from ABAP.


🧱 ABAP Class Structure

We created an ABAP class to manage all interactions with the GitLab API. The class contains the following global fields:

1
2
3
4
5
6
CONSTANTS: gc_api_prefix TYPE string VALUE '/api/v4/'.

DATA: gv_host_name       TYPE string,
      gv_repository_name TYPE string,
      gv_branch_name     TYPE string,
      gv_auth_token      TYPE string.

These fields get initialized by the constructor call. For example:

1
2
3
4
DATA(lo_gitlab) = NEW zcl_gitlab( iv_host_name = <GITLAB_HOST>
                                  iv_repository_name = <GITLAB_REPO>
                                  iv_branch_name = 'master'
                                  iv_auth_token = <GITLAB_AUTH_TOKEN> ).

📂 Listing Files in a Repository

To list all files in a repository, we use the List repository tree endpoint. Here’s an example method that calls this endpoint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
METHOD get_file_listing.
  TYPES: BEGIN OF t_file,
           path TYPE string,
           name TYPE string,
           type TYPE string,
         END OF t_file.

  CONSTANTS:  lc_service_path TYPE string VALUE 'projects/{id}/repository/tree?ref={branch}'.

  DATA: lt_files TYPE TABLE OF t_file.

  " prepare the service path for the given project
  DATA(lv_service_path) = |{ gc_api_prefix }{ lc_service_path }|.
  REPLACE ALL OCCURRENCES OF '{id}'
    IN lv_service_path
    WITH escape( val = gv_repository_name format = cl_abap_format=>e_url_full ).
  REPLACE ALL OCCURRENCES OF '{branch}'
    IN lv_service_path
    WITH escape( val = gv_branch_name format = cl_abap_format=>e_url_full ).

  IF iv_subfolder IS NOT INITIAL.
    lv_service_path = |{ lv_service_path }&path={ escape( val = iv_subfolder format = cl_abap_format=>e_url_full ) }|.
  ENDIF.

  " create the http client
  cl_http_client=>create( EXPORTING host = gv_host_name scheme = cl_http_client=>schemetype_https
                          IMPORTING client = lo_http_client
                          EXCEPTIONS OTHERS = 4 ).
  IF sy-subrc NE 0. RETURN. ENDIF.

  cl_http_utility=>set_request_uri( request = lo_http_client->request uri = lv_service_path ).

  IF gv_auth_token IS NOT INITIAL.
    lo_http_client->request->set_header_field( name = 'PRIVATE-TOKEN' value = gv_auth_token ).
  ENDIF.

  lo_http_client->send( EXCEPTIONS  OTHERS = 4 ).
  IF sy-subrc NE 0.
    " get the error message
    lo_http_client->get_last_error( IMPORTING message = DATA(lv_send_message) ).

    RAISE EXCEPTION TYPE zcx_gitlab
      EXPORTING
        textid = VALUE #( msgid = '00' msgno = '001' attr1 = lv_send_message ).
  ENDIF.

  lo_http_client->receive( EXCEPTIONS OTHERS = 4 ).
  IF sy-subrc NE 0.
    " get the error message
    lo_http_client->get_last_error( IMPORTING message = DATA(lv_receive_message) ).

    RAISE EXCEPTION TYPE zcx_gitlab
      EXPORTING
        textid = VALUE #( msgid = '00' msgno = '001' attr1 = lv_receive_message ).
  ENDIF.

  DATA(lv_response) = lo_http_client->response->get_cdata( ).
  lo_http_client->close( ).

  " if the result contains "message": * there is a problem
  IF lv_response CS '"message"'.
    RAISE EXCEPTION TYPE zcx_gitlab
      EXPORTING
        textid = VALUE #( msgid = '00' msgno = '001' attr1 = lv_response ).
  ENDIF.

  " now unmarshall the JSON response
  /ui2/cl_json=>deserialize( EXPORTING json = lv_response
                              CHANGING data = lt_files ).

  LOOP AT lt_files ASSIGNING FIELD-SYMBOL(<file>) WHERE type NE 'tree'.
    APPEND INITIAL LINE TO rt_files
      ASSIGNING FIELD-SYMBOL(<return>).

    <return>-path_and_name = <file>-path.
    <return>-file_name = <file>-name.
  ENDLOOP.

ENDMETHOD.

📥 Receiving a File from GitLab

To retrieve a file from GitLab, we use the Get file from repository endpoint. Here’s the method for this process:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
METHOD receive_file.
  TYPES: BEGIN OF t_file,
           file_name TYPE string,
           file_path TYPE string,
           size      TYPE i,
           encoding  TYPE string,
           content   TYPE string,
         END OF t_file.

  CONSTANTS: lc_service_path TYPE string VALUE 'projects/{id}/repository/files/{filename}?ref={branch}'.

  DATA: ls_file TYPE t_file.

  " prepare the service path for the given project
  DATA(lv_service_path) = |{ gc_api_prefix }{ lc_service_path }|.
  REPLACE ALL OCCURRENCES OF '{id}'
    IN lv_service_path
    WITH escape( val = gv_repository_name format = cl_abap_format=>e_url_full ).
  REPLACE ALL OCCURRENCES OF '{filename}'
    IN lv_service_path
    WITH escape( val = iv_filename format = cl_abap_format=>e_url_full ).
  REPLACE ALL OCCURRENCES OF '{branch}'
    IN lv_service_path
    WITH escape( val = gv_branch_name format = cl_abap_format=>e_url_full ).

  " create the http client
  cl_http_client=>create( EXPORTING host = gv_host_name scheme = cl_http_client=>schemetype_https
                          IMPORTING client = DATA(lo_http_client)
                         EXCEPTIONS OTHERS = 4 ).
  IF sy-subrc NE 0. RETURN. ENDIF.

  cl_http_utility=>set_request_uri( request = lo_http_client->request uri= lv_service_path ).

  IF gv_auth_token IS NOT INITIAL.
    lo_http_client->request->set_header_field( name  = 'PRIVATE-TOKEN' value = gv_auth_token ).
  ENDIF.

  lo_http_client->send( EXCEPTIONS  OTHERS = 4 ).
  IF sy-subrc NE 0.
    " get the error message
    lo_http_client->get_last_error( IMPORTING message = DATA(lv_send_message) ).

    RAISE EXCEPTION TYPE zcx_gitlab
      EXPORTING
        textid = VALUE #( msgid = '00' msgno = '001' attr1 = lv_send_message ).
  ENDIF.

  lo_http_client->receive( EXCEPTIONS OTHERS = 4 ).
  IF sy-subrc NE 0.
    " get the error message
    lo_http_client->get_last_error( IMPORTING message = DATA(lv_receive_message) ).

    RAISE EXCEPTION TYPE zcx_gitlab
      EXPORTING
        textid = VALUE #( msgid = '00' msgno = '001' attr1 = lv_receive_message ).
  ENDIF.

  DATA(lv_response) = lo_http_client->response->get_cdata( ).
  lo_http_client->close( ).

  " if the result contains "message": * there is a problem
  IF lv_response CS '"message"'.
    RAISE EXCEPTION TYPE zcx_gitlab
      EXPORTING
        textid = VALUE #( msgid = '00' msgno = '001' attr1 = lv_response ).
  ENDIF.

  " now unmarshall the JSON response
  /ui2/cl_json=>deserialize( EXPORTING json = lv_response
                              CHANGING data = ls_file ).

  " only accept base64 encoded files
  IF ls_file-encoding NE 'base64'.
    RAISE EXCEPTION TYPE zcx_gitlab
      EXPORTING
        textid = VALUE #( msgid = '00' msgno = '001' attr1 = 'no BASE64 encoded response' ).
  ENDIF.

  " decode the base64 encoded file
  rv_file_content = cl_http_utility=>decode_x_base64( ls_file-content ).
ENDMETHOD.    

📤 Sending Files to GitLab

Sending files is a bit more complex, as GitLab offers two separate endpoints: Create new file in repository and Update existing file in repository. Since there is no combined “create or update” endpoint, we first check if the file exists.

Here’s the method that checks for file existence:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
METHOD prepare_send_files.

  DATA: lv_path TYPE filep.
  DATA: ls_file TYPE t_file_send_internal.
  DATA: lt_file_listing TYPE tt_file_listing,
        lt_paths        TYPE TABLE OF filep.

  " prepare the given files
  LOOP AT it_files ASSIGNING FIELD-SYMBOL(<import>) WHERE path_and_name IS NOT INITIAL
                                                      AND content IS NOT INITIAL.
    ls_file-filename = <import>-path_and_name.
    ls_file-content_base64 = cl_http_utility=>encode_x_base64( <import>-content ).
    APPEND ls_file
      TO rt_files.

    " extract the path portion for the existing check later
    CLEAR lv_path.
    CALL FUNCTION 'CV120_SPLIT_PATH'
      EXPORTING
        pf_path  = CONV char1024( <import>-path_and_name )
      IMPORTING
        pfx_path = lv_path.
    APPEND lv_path
      TO lt_paths.
  ENDLOOP.

  CHECK rt_files IS NOT INITIAL.
  SORT lt_paths.
  DELETE ADJACENT DUPLICATES FROM lt_paths.

  " check whether the file already exists or not
  LOOP AT lt_paths ASSIGNING FIELD-SYMBOL(<path>).
    APPEND LINES OF get_file_listing( iv_subfolder = CONV #( <path> ) )
      TO lt_file_listing.
  ENDLOOP.

  LOOP AT rt_files ASSIGNING FIELD-SYMBOL(<file>).
    READ TABLE lt_file_listing WITH KEY path_and_name = <file>-filename
                               TRANSPORTING NO FIELDS.
    IF sy-subrc EQ 0.
      <file>-action = 'update'.
    ELSE.
      <file>-action = 'create'.
    ENDIF.

  ENDLOOP.

ENDMETHOD.

Having this helper method allows us to use a single method to send files to the GitLab repository:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
METHOD send_files.
  CONSTANTS: lc_service_path TYPE string VALUE 'projects/{id}/repository/commits'.

  TYPES: BEGIN OF t_action,
           action    TYPE string,
           file_path TYPE string,
           content   TYPE string,
           encoding  TYPE string,
         END OF t_action.

  TYPES: tt_action TYPE STANDARD TABLE OF t_action
                  WITH DEFAULT KEY.

  TYPES: BEGIN OF t_body,
           branch         TYPE string,
           commit_message TYPE string,
           actions        TYPE tt_action,
         END OF t_body.

  DATA: lv_body TYPE string.
  DATA: ls_body   TYPE t_body,
        ls_action TYPE t_action.

  " prepare the files to be send
  DATA(lt_files) = prepare_send_files( it_files ).

  " build the POST body
  ls_body-branch = gv_branch_name.
  ls_body-commit_message = iv_commit_message.

  LOOP AT lt_files ASSIGNING FIELD-SYMBOL(<file>).
    CLEAR: ls_action.

    ls_action-action = <file>-action.
    ls_action-file_path = <file>-filename.
    ls_action-content = <file>-content_base64.
    ls_action-encoding = 'base64'.
    APPEND ls_action
      TO ls_body-actions.

  ENDLOOP.

  lv_body = /ui2/cl_json=>serialize( data = ls_body pretty_name = /ui2/cl_json=>pretty_mode-low_case ).

  " prepare the service path for the given project
  DATA(lv_service_path) = |{ gc_api_prefix }{ lc_service_path }|.
  REPLACE ALL OCCURRENCES OF '{id}'
    IN lv_service_path
    WITH escape( val = gv_repository_name format = cl_abap_format=>e_url_full ).

  " create the http client
  cl_http_client=>create( EXPORTING host = gv_host_name scheme = cl_http_client=>schemetype_https
                          IMPORTING client = DATA(lo_http_client)
                         EXCEPTIONS OTHERS = 4 ).
  IF sy-subrc NE 0. RETURN. ENDIF.

  cl_http_utility=>set_request_uri( request = lo_http_client->request uri = lv_service_path ).

  IF gv_auth_token IS NOT INITIAL.
    lo_http_client->request->set_header_field( name  = 'PRIVATE-TOKEN' value = gv_auth_token ).
  ENDIF.

  lo_http_client->request->set_method( method = if_http_request=>co_request_method_post ).
  lo_http_client->request->set_header_field( name  = 'Content-Type' value = 'application/json' ).
  lo_http_client->request->set_cdata( data = lv_body ).

  lo_http_client->send( EXCEPTIONS  OTHERS = 4 ).
  IF sy-subrc NE 0.
    " get the error message
    lo_http_client->get_last_error( IMPORTING message = DATA(lv_send_message) ).

    RAISE EXCEPTION TYPE zcx_gitlab
      EXPORTING
        textid = VALUE #( msgid = '00' msgno = '001' attr1 = lv_send_message ).
  ENDIF.

  lo_http_client->receive( EXCEPTIONS OTHERS = 4 ).
  IF sy-subrc NE 0.
    " get the error message
    lo_http_client->get_last_error( IMPORTING message = DATA(lv_receive_message) ).

    RAISE EXCEPTION TYPE zcx_gitlab
      EXPORTING
        textid = VALUE #( msgid = '00' msgno = '001' attr1 = lv_receive_message ).
  ENDIF.

  DATA(lv_response) = lo_http_client->response->get_cdata( ).
  lo_http_client->close( ).

ENDMETHOD.

With this class, you can now interact with GitLab to list, receive, and send files. You can modify the existing functionality as needed to handle more use cases.


🧠 Final Thoughts

Integrating GitLab with ABAP for file management opens up numerous possibilities for automating and streamlining processes, especially for tasks like translation file management. By leveraging the GitLab API, we’ve been able to seamlessly interact with Git repositories directly from ABAP, without needing local Git tools. This approach enhances flexibility and simplifies the management of repository files within SAP systems.

With the methods outlined in this article, you can easily extend the functionality to interact with GitLab repositories for tasks like listing files, receiving file contents, and updating or creating files. Whether you’re integrating for translation management or other use cases, this ABAP-GitLab integration provides a powerful solution for automating SAP-GitLab interactions.

By implementing this system, you can reduce manual intervention, streamline workflows, and improve the overall efficiency of managing repository data within your SAP environment.

This post is licensed under CC BY 4.0 by the author.