GitLab API via ABAP
GitLab offers a nice API to work with Git without having the Git runtime/tools installed. This becomes handy when trying to access a Git repository from ABAP code.
In our Weblate integration we needed the option to transfer generated translation files to/from GitLab. To achieve this, we implemented several API endpoints of the GitLab API in an ABAP class:
Prerequisites
To access the GitLab API via HTTP you may need to setup the GitLab server via transaction SM59
if you don’t want to insert the server name via literals/constants in your ABAP code. We’ve decided to provide the options to work with multiple servers without SM59
(hostnames only).
For an anonymous access to the GitLab API you need to setup a personal access token which can be used with the HTTP call from ABAP.
Class structure
We’ve created an ABAP class to handle all requests to the GitLab API. Inside the class we’ve 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> ).
File listing
To get a list of all files in a repository the endpoint List repository tree can be used. A method using this endpoint could look like:
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.
Receive file
To receive files via the API the endpoint Get file from repository can be used. Our method implementing the endpoint looks like:
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.
Send file
Sending files to the GitLab API is a bit more complicated than receiving. There are two different API endpoints: Create new file in repository and Update existing file in repository. Since there is no endpoint like “create or update” we need to check whether the file exists before we actually send it.
For this check we’ve implemented an own method:
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 toolbox of methods you are able to send/receive any sort of files in ABAP.