Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
G
git2
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
lvzhengyang
git2
Commits
074d323f
Commit
074d323f
authored
May 04, 2015
by
Carlos Martín Nieto
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3079 from ethomson/config
Configuration changes for handling multiple of the same sections
parents
c3414d53
d6b7e404
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
674 additions
and
434 deletions
+674
-434
src/config_file.c
+472
-434
tests/config/write.c
+202
-0
No files found.
src/config_file.c
View file @
074d323f
...
...
@@ -114,8 +114,7 @@ typedef struct {
diskfile_backend
*
snapshot_from
;
}
diskfile_readonly_backend
;
static
int
config_parse
(
git_strmap
*
values
,
diskfile_backend
*
cfg_file
,
struct
reader
*
reader
,
git_config_level_t
level
,
int
depth
);
static
int
parse_variable
(
struct
reader
*
reader
,
char
**
var_name
,
char
**
var_value
);
static
int
config_read
(
git_strmap
*
values
,
diskfile_backend
*
cfg_file
,
struct
reader
*
reader
,
git_config_level_t
level
,
int
depth
);
static
int
config_write
(
diskfile_backend
*
cfg
,
const
char
*
key
,
const
regex_t
*
preg
,
const
char
*
value
);
static
char
*
escape_value
(
const
char
*
ptr
);
...
...
@@ -288,7 +287,7 @@ static int config_open(git_config_backend *cfg, git_config_level_t level)
if
(
res
==
GIT_ENOTFOUND
)
return
0
;
if
(
res
<
0
||
(
res
=
config_
parse
(
b
->
header
.
values
->
values
,
b
,
reader
,
level
,
0
))
<
0
)
{
if
(
res
<
0
||
(
res
=
config_
read
(
b
->
header
.
values
->
values
,
b
,
reader
,
level
,
0
))
<
0
)
{
refcounted_strmap_free
(
b
->
header
.
values
);
b
->
header
.
values
=
NULL
;
}
...
...
@@ -313,7 +312,7 @@ static int config__refresh(git_config_backend *cfg)
reader
=
git_array_get
(
b
->
readers
,
git_array_size
(
b
->
readers
)
-
1
);
GITERR_CHECK_ALLOC
(
reader
);
if
((
error
=
config_
parse
(
values
->
values
,
b
,
reader
,
b
->
level
,
0
))
<
0
)
if
((
error
=
config_
read
(
values
->
values
,
b
,
reader
,
b
->
level
,
0
))
<
0
)
goto
out
;
git_mutex_lock
(
&
b
->
header
.
values_mutex
);
...
...
@@ -831,7 +830,7 @@ static int reader_getchar_raw(struct reader *reader)
if
(
c
==
0
)
{
reader
->
eof
=
1
;
c
=
'\
n
'
;
c
=
'\
0
'
;
}
return
c
;
...
...
@@ -850,13 +849,12 @@ static int reader_getchar(struct reader *reader, int flags)
do
{
c
=
reader_getchar_raw
(
reader
);
}
while
(
skip_whitespace
&&
git__isspace
(
c
)
&&
!
reader
->
eof
);
}
while
(
c
!=
'\n'
&&
c
!=
'\0'
&&
skip_whitespace
&&
git__isspace
(
c
));
if
(
skip_comments
&&
(
c
==
'#'
||
c
==
';'
))
{
do
{
c
=
reader_getchar_raw
(
reader
);
}
while
(
c
!=
'\n'
);
}
while
(
c
!=
'\n'
&&
c
!=
'\0'
);
}
return
c
;
...
...
@@ -1200,57 +1198,314 @@ static int included_path(git_buf *out, const char *dir, const char *path)
return
git_path_join_unrooted
(
out
,
path
,
dir
,
NULL
);
}
static
int
config_parse
(
git_strmap
*
values
,
diskfile_backend
*
cfg_file
,
struct
reader
*
reader
,
git_config_level_t
level
,
int
depth
)
static
const
char
*
escapes
=
"ntb
\"\\
"
;
static
const
char
*
escaped
=
"
\n\t\b\"\\
"
;
/* Escape the values to write them to the file */
static
char
*
escape_value
(
const
char
*
ptr
)
{
int
c
;
char
*
current_section
=
NULL
;
char
*
var_name
;
char
*
var_value
;
cvar_t
*
var
;
git_buf
buf
=
GIT_BUF_INIT
;
int
result
=
0
;
uint32_t
reader_idx
;
size_t
len
;
const
char
*
esc
;
if
(
depth
>=
MAX_INCLUDE_DEPTH
)
{
giterr_set
(
GITERR_CONFIG
,
"Maximum config include depth reached"
);
assert
(
ptr
);
len
=
strlen
(
ptr
);
if
(
!
len
)
return
git__calloc
(
1
,
sizeof
(
char
));
git_buf_grow
(
&
buf
,
len
);
while
(
*
ptr
!=
'\0'
)
{
if
((
esc
=
strchr
(
escaped
,
*
ptr
))
!=
NULL
)
{
git_buf_putc
(
&
buf
,
'\\'
);
git_buf_putc
(
&
buf
,
escapes
[
esc
-
escaped
]);
}
else
{
git_buf_putc
(
&
buf
,
*
ptr
);
}
ptr
++
;
}
if
(
git_buf_oom
(
&
buf
))
{
git_buf_free
(
&
buf
);
return
NULL
;
}
return
git_buf_detach
(
&
buf
);
}
/* '\"' -> '"' etc */
static
int
unescape_line
(
char
**
out
,
bool
*
is_multi
,
const
char
*
ptr
,
int
quote_count
)
{
char
*
str
,
*
fixed
,
*
esc
;
size_t
ptr_len
=
strlen
(
ptr
),
alloc_len
;
*
is_multi
=
false
;
if
(
GIT_ADD_SIZET_OVERFLOW
(
&
alloc_len
,
ptr_len
,
1
)
||
(
str
=
git__malloc
(
alloc_len
))
==
NULL
)
{
return
-
1
;
}
reader_idx
=
git_array_size
(
cfg_file
->
readers
)
-
1
;
/* Initialize the reading position */
reader
->
read_ptr
=
reader
->
buffer
.
ptr
;
reader
->
eof
=
0
;
fixed
=
str
;
/* If the file is empty, there's nothing for us to do */
if
(
*
reader
->
read_ptr
==
'\0'
)
while
(
*
ptr
!=
'\0'
)
{
if
(
*
ptr
==
'"'
)
{
quote_count
++
;
}
else
if
(
*
ptr
!=
'\\'
)
{
*
fixed
++
=
*
ptr
;
}
else
{
/* backslash, check the next char */
ptr
++
;
/* if we're at the end, it's a multiline, so keep the backslash */
if
(
*
ptr
==
'\0'
)
{
*
is_multi
=
true
;
goto
done
;
}
if
((
esc
=
strchr
(
escapes
,
*
ptr
))
!=
NULL
)
{
*
fixed
++
=
escaped
[
esc
-
escapes
];
}
else
{
git__free
(
str
);
giterr_set
(
GITERR_CONFIG
,
"Invalid escape at %s"
,
ptr
);
return
-
1
;
}
}
ptr
++
;
}
done:
*
fixed
=
'\0'
;
*
out
=
str
;
return
0
;
}
static
int
parse_multiline_variable
(
struct
reader
*
reader
,
git_buf
*
value
,
int
in_quotes
)
{
char
*
line
=
NULL
,
*
proc_line
=
NULL
;
int
quote_count
;
bool
multiline
;
/* Check that the next line exists */
line
=
reader_readline
(
reader
,
false
);
if
(
line
==
NULL
)
return
-
1
;
/* We've reached the end of the file, there is no continuation.
* (this is not an error).
*/
if
(
line
[
0
]
==
'\0'
)
{
git__free
(
line
);
return
0
;
}
quote_count
=
strip_comments
(
line
,
!!
in_quotes
);
/* If it was just a comment, pretend it didn't exist */
if
(
line
[
0
]
==
'\0'
)
{
git__free
(
line
);
return
parse_multiline_variable
(
reader
,
value
,
quote_count
);
/* TODO: unbounded recursion. This **could** be exploitable */
}
if
(
unescape_line
(
&
proc_line
,
&
multiline
,
line
,
in_quotes
)
<
0
)
{
git__free
(
line
);
return
-
1
;
}
/* add this line to the multiline var */
git_buf_puts
(
value
,
proc_line
);
git__free
(
line
);
git__free
(
proc_line
);
/*
* If we need to continue reading the next line, let's just
* keep putting stuff in the buffer
*/
if
(
multiline
)
return
parse_multiline_variable
(
reader
,
value
,
quote_count
);
return
0
;
}
GIT_INLINE
(
bool
)
is_namechar
(
char
c
)
{
return
isalnum
(
c
)
||
c
==
'-'
;
}
static
int
parse_name
(
char
**
name
,
const
char
**
value
,
struct
reader
*
reader
,
const
char
*
line
)
{
const
char
*
name_end
=
line
,
*
value_start
;
*
name
=
NULL
;
*
value
=
NULL
;
while
(
*
name_end
&&
is_namechar
(
*
name_end
))
name_end
++
;
if
(
line
==
name_end
)
{
set_parse_error
(
reader
,
0
,
"Invalid configuration key"
);
return
-
1
;
}
value_start
=
name_end
;
while
(
*
value_start
&&
git__isspace
(
*
value_start
))
value_start
++
;
if
(
*
value_start
==
'='
)
{
*
value
=
value_start
+
1
;
}
else
if
(
*
value_start
)
{
set_parse_error
(
reader
,
0
,
"Invalid configuration key"
);
return
-
1
;
}
if
((
*
name
=
git__strndup
(
line
,
name_end
-
line
))
==
NULL
)
return
-
1
;
return
0
;
}
static
int
parse_variable
(
struct
reader
*
reader
,
char
**
var_name
,
char
**
var_value
)
{
const
char
*
value_start
=
NULL
;
char
*
line
;
int
quote_count
;
bool
multiline
;
line
=
reader_readline
(
reader
,
true
);
if
(
line
==
NULL
)
return
-
1
;
quote_count
=
strip_comments
(
line
,
0
);
if
(
parse_name
(
var_name
,
&
value_start
,
reader
,
line
)
<
0
)
return
-
1
;
/* If there is no value, boolean true is assumed */
*
var_value
=
NULL
;
/*
* Now, let's try to parse the value
*/
if
(
value_start
!=
NULL
)
{
while
(
git__isspace
(
value_start
[
0
]))
value_start
++
;
if
(
unescape_line
(
var_value
,
&
multiline
,
value_start
,
0
)
<
0
)
goto
on_error
;
if
(
multiline
)
{
git_buf
multi_value
=
GIT_BUF_INIT
;
git_buf_attach
(
&
multi_value
,
*
var_value
,
0
);
if
(
parse_multiline_variable
(
reader
,
&
multi_value
,
quote_count
)
<
0
||
git_buf_oom
(
&
multi_value
))
{
git_buf_free
(
&
multi_value
);
goto
on_error
;
}
*
var_value
=
git_buf_detach
(
&
multi_value
);
}
}
git__free
(
line
);
return
0
;
on_error
:
git__free
(
*
var_name
);
git__free
(
line
);
return
-
1
;
}
static
int
config_parse
(
struct
reader
*
reader
,
int
(
*
on_section
)(
struct
reader
**
reader
,
const
char
*
current_section
,
const
char
*
line
,
size_t
line_len
,
void
*
data
),
int
(
*
on_variable
)(
struct
reader
**
reader
,
const
char
*
current_section
,
char
*
var_name
,
char
*
var_value
,
const
char
*
line
,
size_t
line_len
,
void
*
data
),
int
(
*
on_comment
)(
struct
reader
**
reader
,
const
char
*
line
,
size_t
line_len
,
void
*
data
),
int
(
*
on_eof
)(
struct
reader
**
reader
,
void
*
data
),
void
*
data
)
{
char
*
current_section
=
NULL
,
*
var_name
,
*
var_value
,
*
line_start
;
char
c
;
size_t
line_len
;
int
result
=
0
;
skip_bom
(
reader
);
while
(
result
==
0
&&
!
reader
->
eof
)
{
line_start
=
reader
->
read_ptr
;
c
=
reader_peek
(
reader
,
SKIP_WHITESPACE
);
switch
(
c
)
{
case
'\
n
'
:
/* EOF when peeking, set EOF in the reader to exit the loop */
case
'\
0
'
:
/* EOF when peeking, set EOF in the reader to exit the loop */
reader
->
eof
=
1
;
break
;
case
'['
:
/* section header, new section begins */
git__free
(
current_section
);
current_section
=
NULL
;
result
=
parse_section_header
(
reader
,
&
current_section
);
if
((
result
=
parse_section_header
(
reader
,
&
current_section
))
==
0
&&
on_section
)
{
line_len
=
reader
->
read_ptr
-
line_start
;
result
=
on_section
(
&
reader
,
current_section
,
line_start
,
line_len
,
data
);
}
break
;
case
'\n'
:
/* comment or whitespace-only */
case
';'
:
case
'#'
:
reader_consume_line
(
reader
);
if
(
on_comment
)
{
line_len
=
reader
->
read_ptr
-
line_start
;
result
=
on_comment
(
&
reader
,
line_start
,
line_len
,
data
);
}
break
;
default
:
/* assume variable declaration */
result
=
parse_variable
(
reader
,
&
var_name
,
&
var_value
);
if
(
result
<
0
)
if
((
result
=
parse_variable
(
reader
,
&
var_name
,
&
var_value
))
==
0
&&
on_variable
)
{
line_len
=
reader
->
read_ptr
-
line_start
;
result
=
on_variable
(
&
reader
,
current_section
,
var_name
,
var_value
,
line_start
,
line_len
,
data
);
}
break
;
}
}
if
(
on_eof
)
result
=
on_eof
(
&
reader
,
data
);
git__free
(
current_section
);
return
result
;
}
struct
parse_data
{
git_strmap
*
values
;
diskfile_backend
*
cfg_file
;
uint32_t
reader_idx
;
git_config_level_t
level
;
int
depth
;
};
static
int
read_on_variable
(
struct
reader
**
reader
,
const
char
*
current_section
,
char
*
var_name
,
char
*
var_value
,
const
char
*
line
,
size_t
line_len
,
void
*
data
)
{
struct
parse_data
*
parse_data
=
(
struct
parse_data
*
)
data
;
git_buf
buf
=
GIT_BUF_INIT
;
cvar_t
*
var
;
int
result
=
0
;
GIT_UNUSED
(
line
);
GIT_UNUSED
(
line_len
);
git__strtolower
(
var_name
);
git_buf_printf
(
&
buf
,
"%s.%s"
,
current_section
,
var_name
);
...
...
@@ -1268,13 +1523,12 @@ static int config_parse(git_strmap *values, diskfile_backend *cfg_file, struct r
var
->
entry
->
name
=
git_buf_detach
(
&
buf
);
var
->
entry
->
value
=
var_value
;
var
->
entry
->
level
=
level
;
var
->
included
=
!!
depth
;
var
->
entry
->
level
=
parse_data
->
level
;
var
->
included
=
!!
parse_data
->
depth
;
if
((
result
=
append_entry
(
parse_data
->
values
,
var
))
<
0
)
return
result
;
if
((
result
=
append_entry
(
values
,
var
))
<
0
)
break
;
else
result
=
0
;
/* Add or append the new config option */
...
...
@@ -1284,49 +1538,68 @@ static int config_parse(git_strmap *values, diskfile_backend *cfg_file, struct r
char
*
dir
;
uint32_t
index
;
r
=
git_array_alloc
(
cfg_file
->
readers
);
r
=
git_array_alloc
(
parse_data
->
cfg_file
->
readers
);
/* The reader may have been reallocated */
reader
=
git_array_get
(
cfg_file
->
readers
,
reader_idx
);
*
reader
=
git_array_get
(
parse_data
->
cfg_file
->
readers
,
parse_data
->
reader_idx
);
memset
(
r
,
0
,
sizeof
(
struct
reader
));
if
((
result
=
git_path_dirname_r
(
&
path
,
reader
->
file_path
))
<
0
)
break
;
if
((
result
=
git_path_dirname_r
(
&
path
,
(
*
reader
)
->
file_path
))
<
0
)
return
result
;
/* We need to know our index in the array, as the next config_parse call may realloc */
index
=
git_array_size
(
cfg_file
->
readers
)
-
1
;
index
=
git_array_size
(
parse_data
->
cfg_file
->
readers
)
-
1
;
dir
=
git_buf_detach
(
&
path
);
result
=
included_path
(
&
path
,
dir
,
var
->
entry
->
value
);
git__free
(
dir
);
if
(
result
<
0
)
break
;
return
result
;
r
->
file_path
=
git_buf_detach
(
&
path
);
git_buf_init
(
&
r
->
buffer
,
0
);
result
=
git_futils_readbuffer_updated
(
&
r
->
buffer
,
r
->
file_path
,
&
r
->
file_mtime
,
&
r
->
file_size
,
NULL
);
result
=
git_futils_readbuffer_updated
(
&
r
->
buffer
,
r
->
file_path
,
&
r
->
file_mtime
,
&
r
->
file_size
,
NULL
);
if
(
result
==
0
)
{
result
=
config_parse
(
values
,
cfg_file
,
r
,
level
,
depth
+
1
);
r
=
git_array_get
(
cfg_file
->
readers
,
index
);
reader
=
git_array_get
(
cfg_file
->
readers
,
reader_idx
);
}
else
if
(
result
==
GIT_ENOTFOUND
)
{
result
=
config_read
(
parse_data
->
values
,
parse_data
->
cfg_file
,
r
,
parse_data
->
level
,
parse_data
->
depth
+
1
);
r
=
git_array_get
(
parse_data
->
cfg_file
->
readers
,
index
);
*
reader
=
git_array_get
(
parse_data
->
cfg_file
->
readers
,
parse_data
->
reader_idx
);
}
else
if
(
result
==
GIT_ENOTFOUND
)
{
giterr_clear
();
result
=
0
;
}
git_buf_free
(
&
r
->
buffer
);
if
(
result
<
0
)
break
;
}
break
;
}
return
result
;
}
static
int
config_read
(
git_strmap
*
values
,
diskfile_backend
*
cfg_file
,
struct
reader
*
reader
,
git_config_level_t
level
,
int
depth
)
{
struct
parse_data
parse_data
;
if
(
depth
>=
MAX_INCLUDE_DEPTH
)
{
giterr_set
(
GITERR_CONFIG
,
"Maximum config include depth reached"
);
return
-
1
;
}
git__free
(
current_section
);
return
result
;
/* Initialize the reading position */
reader
->
read_ptr
=
reader
->
buffer
.
ptr
;
reader
->
eof
=
0
;
/* If the file is empty, there's nothing for us to do */
if
(
*
reader
->
read_ptr
==
'\0'
)
return
0
;
parse_data
.
values
=
values
;
parse_data
.
cfg_file
=
cfg_file
;
parse_data
.
reader_idx
=
git_array_size
(
cfg_file
->
readers
)
-
1
;
parse_data
.
level
=
level
;
parse_data
.
depth
=
depth
;
return
config_parse
(
reader
,
NULL
,
read_on_variable
,
NULL
,
NULL
,
&
parse_data
);
}
static
int
write_section
(
git_filebuf
*
file
,
const
char
*
key
)
...
...
@@ -1377,439 +1650,204 @@ static const char *quotes_for_value(const char *value)
return
""
;
}
/*
* This is pretty much the parsing, except we write out anything we don't have
*/
static
int
config_write
(
diskfile_backend
*
cfg
,
const
char
*
key
,
const
regex_t
*
preg
,
const
char
*
value
)
{
int
result
,
c
;
int
section_matches
=
0
,
last_section_matched
=
0
,
preg_replaced
=
0
,
write_trailer
=
0
;
const
char
*
pre_end
=
NULL
,
*
post_start
=
NULL
,
*
data_start
,
*
write_start
;
char
*
current_section
=
NULL
,
*
section
,
*
name
,
*
ldot
;
git_filebuf
file
=
GIT_FILEBUF_INIT
;
struct
reader
*
reader
=
git_array_get
(
cfg
->
readers
,
0
);
/* We need to read in our own config file */
result
=
git_futils_readbuffer
(
&
reader
->
buffer
,
cfg
->
file_path
);
struct
write_data
{
git_filebuf
*
file
;
unsigned
int
in_section
:
1
,
preg_replaced
:
1
;
const
char
*
section
;
const
char
*
name
;
const
regex_t
*
preg
;
const
char
*
value
;
};
/* Initialise the reading position */
if
(
result
==
GIT_ENOTFOUND
)
{
reader
->
read_ptr
=
NULL
;
reader
->
eof
=
1
;
data_start
=
NULL
;
git_buf_clear
(
&
reader
->
buffer
);
}
else
if
(
result
==
0
)
{
reader
->
read_ptr
=
reader
->
buffer
.
ptr
;
reader
->
eof
=
0
;
data_start
=
reader
->
read_ptr
;
}
else
{
return
-
1
;
/* OS error when reading the file */
}
static
int
write_line
(
struct
write_data
*
write_data
,
const
char
*
line
,
size_t
line_len
)
{
int
result
=
git_filebuf_write
(
write_data
->
file
,
line
,
line_len
);
write_start
=
data_start
;
if
(
!
result
&&
line_len
&&
line
[
line_len
-
1
]
!=
'\n'
)
result
=
git_filebuf_printf
(
write_data
->
file
,
"
\n
"
);
/* Lock the file */
if
((
result
=
git_filebuf_open
(
&
file
,
cfg
->
file_path
,
0
,
GIT_CONFIG_FILE_MODE
))
<
0
)
{
git_buf_free
(
&
reader
->
buffer
);
return
result
;
}
skip_bom
(
reader
);
ldot
=
strrchr
(
key
,
'.'
);
name
=
ldot
+
1
;
section
=
git__strndup
(
key
,
ldot
-
key
);
while
(
!
reader
->
eof
)
{
c
=
reader_peek
(
reader
,
SKIP_WHITESPACE
);
if
(
c
==
'\n'
)
{
/* We've arrived at the end of the file */
break
;
}
else
if
(
c
==
'['
)
{
/* section header, new section begins */
/*
* We set both positions to the current one in case we
* need to add a variable to the end of a section. In that
* case, we want both variables to point just before the
* new section. If we actually want to replace it, the
* default case will take care of updating them.
*/
pre_end
=
post_start
=
reader
->
read_ptr
;
git__free
(
current_section
);
current_section
=
NULL
;
if
(
parse_section_header
(
reader
,
&
current_section
)
<
0
)
goto
rewrite_fail
;
/* Keep track of when it stops matching */
last_section_matched
=
section_matches
;
section_matches
=
!
strcmp
(
current_section
,
section
);
}
else
if
(
c
==
';'
||
c
==
'#'
)
{
reader_consume_line
(
reader
);
}
else
{
/*
* If the section doesn't match, but the last section did,
* it means we need to add a variable (so skip the line
* otherwise). If both the section and name match, we need
* to overwrite the variable (so skip the line
* otherwise). pre_end needs to be updated each time so we
* don't loose that information, but we only need to
* update post_start if we're going to use it in this
* iteration.
* If the section doesn't match and we are trying to delete an entry
* (value == NULL), we must continue searching; there may be another
* matching section later.
*/
if
(
!
section_matches
)
{
if
(
!
last_section_matched
||
value
==
NULL
)
{
reader_consume_line
(
reader
);
continue
;
}
}
else
{
int
has_matched
=
0
;
char
*
var_name
,
*
var_value
;
pre_end
=
reader
->
read_ptr
;
if
(
parse_variable
(
reader
,
&
var_name
,
&
var_value
)
<
0
)
goto
rewrite_fail
;
/* First try to match the name of the variable */
if
(
strcasecmp
(
name
,
var_name
)
==
0
)
has_matched
=
1
;
/* If the name matches, and we have a regex to match the
* value, try to match it */
if
(
has_matched
&&
preg
!=
NULL
)
has_matched
=
(
regexec
(
preg
,
var_value
,
0
,
NULL
,
0
)
==
0
);
git__free
(
var_name
);
git__free
(
var_value
);
/* if there is no match, keep going */
if
(
!
has_matched
)
continue
;
post_start
=
reader
->
read_ptr
;
}
/* We've found the variable we wanted to change, so
* write anything up to it */
git_filebuf_write
(
&
file
,
write_start
,
pre_end
-
write_start
);
preg_replaced
=
1
;
/* Then replace the variable. If the value is NULL, it
* means we want to delete it, so don't write anything. */
if
(
value
!=
NULL
)
{
const
char
*
q
=
quotes_for_value
(
value
);
git_filebuf_printf
(
&
file
,
"
\t
%s = %s%s%s
\n
"
,
name
,
q
,
value
,
q
);
}
/*
* If we have a multivar, we should keep looking for entries,
* but only if we're in the right section. Otherwise we'll end up
* looping on the edge of a matching and a non-matching section.
*/
if
(
section_matches
&&
preg
!=
NULL
)
{
write_start
=
post_start
;
continue
;
}
write_trailer
=
1
;
break
;
/* break from the loop */
}
}
}
/*
* Being here can mean that
*
* 1) our section is the last one in the file and we're
* adding a variable
*
* 2) we didn't find a section for us so we need to create it
* ourselves.
*
* 3) we're setting a multivar with a regex, which means we
* continue to search for matching values
*
* In the last case, if we've already replaced a value, we
* want to write the rest of the file. Otherwise we need to write
* out the whole file and then the new variable.
*/
if
(
write_trailer
)
{
/* Write out rest of the file */
git_filebuf_write
(
&
file
,
post_start
,
reader
->
buffer
.
size
-
(
post_start
-
data_start
));
}
else
{
if
(
preg_replaced
)
{
git_filebuf_printf
(
&
file
,
"
\n
%s"
,
write_start
);
}
else
{
static
int
write_value
(
struct
write_data
*
write_data
)
{
const
char
*
q
;
int
result
;
git_filebuf_write
(
&
file
,
reader
->
buffer
.
ptr
,
reader
->
buffer
.
size
);
if
(
reader
->
buffer
.
size
>
0
&&
*
(
reader
->
buffer
.
ptr
+
reader
->
buffer
.
size
-
1
)
!=
'\n'
)
git_filebuf_write
(
&
file
,
"
\n
"
,
1
);
/* And now if we just need to add a variable */
if
(
!
section_matches
&&
write_section
(
&
file
,
section
)
<
0
)
goto
rewrite_fail
;
/* Sanity check: if we are here, and value is NULL, that means that somebody
* touched the config file after our initial read. We should probably assert()
* this, but instead we'll handle it gracefully with an error. */
if
(
value
==
NULL
)
{
giterr_set
(
GITERR_CONFIG
,
"race condition when writing a config file (a cvar has been removed)"
);
goto
rewrite_fail
;
}
/* If we are here, there is at least a section line */
q
=
quotes_for_value
(
value
);
git_filebuf_printf
(
&
file
,
"
\t
%s = %s%s%s
\n
"
,
name
,
q
,
value
,
q
);
}
}
git__free
(
section
);
git__free
(
current_section
);
q
=
quotes_for_value
(
write_data
->
value
);
result
=
git_filebuf_printf
(
write_data
->
file
,
"
\t
%s = %s%s%s
\n
"
,
write_data
->
name
,
q
,
write_data
->
value
,
q
);
/* refresh stats - if this errors, then commit will error too */
(
void
)
git_filebuf_stats
(
&
reader
->
file_mtime
,
&
reader
->
file_size
,
&
file
);
result
=
git_filebuf_commit
(
&
file
);
git_buf_free
(
&
reader
->
buffer
);
/* If we are updating a single name/value, we're done. Setting `value`
* to `NULL` will prevent us from trying to write it again later (in
* `write_on_section`) if we see the same section repeated.
*/
if
(
!
write_data
->
preg
)
write_data
->
value
=
NULL
;
return
result
;
rewrite_fail
:
git__free
(
section
);
git__free
(
current_section
);
git_filebuf_cleanup
(
&
file
);
git_buf_free
(
&
reader
->
buffer
);
return
-
1
;
}
static
const
char
*
escapes
=
"ntb
\"\\
"
;
static
const
char
*
escaped
=
"
\n\t\b\"\\
"
;
/* Escape the values to write them to the file */
static
char
*
escape_value
(
const
char
*
ptr
)
static
int
write_on_section
(
struct
reader
**
reader
,
const
char
*
current_section
,
const
char
*
line
,
size_t
line_len
,
void
*
data
)
{
git_buf
buf
=
GIT_BUF_INIT
;
size_t
len
;
const
char
*
esc
;
struct
write_data
*
write_data
=
(
struct
write_data
*
)
data
;
int
result
=
0
;
assert
(
pt
r
);
GIT_UNUSED
(
reade
r
);
len
=
strlen
(
ptr
);
if
(
!
len
)
return
git__calloc
(
1
,
sizeof
(
char
));
git_buf_grow
(
&
buf
,
len
);
/* If we were previously in the correct section (but aren't anymore)
* and haven't written our value (for a simple name/value set, not
* a multivar), then append it to the end of the section before writing
* the new one.
*/
if
(
write_data
->
in_section
&&
!
write_data
->
preg
&&
write_data
->
value
)
result
=
write_value
(
write_data
);
while
(
*
ptr
!=
'\0'
)
{
if
((
esc
=
strchr
(
escaped
,
*
ptr
))
!=
NULL
)
{
git_buf_putc
(
&
buf
,
'\\'
);
git_buf_putc
(
&
buf
,
escapes
[
esc
-
escaped
]);
}
else
{
git_buf_putc
(
&
buf
,
*
ptr
);
}
ptr
++
;
}
write_data
->
in_section
=
strcmp
(
current_section
,
write_data
->
section
)
==
0
;
if
(
git_buf_oom
(
&
buf
))
{
git_buf_free
(
&
buf
);
return
NULL
;
}
if
(
!
result
)
result
=
write_line
(
write_data
,
line
,
line_len
);
return
git_buf_detach
(
&
buf
)
;
return
result
;
}
/* '\"' -> '"' etc */
static
int
unescape_line
(
char
**
out
,
bool
*
is_multi
,
const
char
*
ptr
,
int
quote_count
)
static
int
write_on_variable
(
struct
reader
**
reader
,
const
char
*
current_section
,
char
*
var_name
,
char
*
var_value
,
const
char
*
line
,
size_t
line_len
,
void
*
data
)
{
char
*
str
,
*
fixed
,
*
esc
;
size_t
ptr_len
=
strlen
(
ptr
),
alloc_len
;
struct
write_data
*
write_data
=
(
struct
write_data
*
)
data
;
bool
has_matched
=
false
;
*
is_multi
=
false
;
GIT_UNUSED
(
reader
);
GIT_UNUSED
(
current_section
);
if
(
GIT_ADD_SIZET_OVERFLOW
(
&
alloc_len
,
ptr_len
,
1
)
||
(
str
=
git__malloc
(
alloc_len
))
==
NULL
)
{
return
-
1
;
}
/* See if we are to update this name/value pair; first examine name */
if
(
write_data
->
in_section
&&
strcasecmp
(
write_data
->
name
,
var_name
)
==
0
)
has_matched
=
true
;
fixed
=
str
;
/* If we have a regex to match the value, see if it matches */
if
(
has_matched
&&
write_data
->
preg
!=
NULL
)
has_matched
=
(
regexec
(
write_data
->
preg
,
var_value
,
0
,
NULL
,
0
)
==
0
);
while
(
*
ptr
!=
'\0'
)
{
if
(
*
ptr
==
'"'
)
{
quote_count
++
;
}
else
if
(
*
ptr
!=
'\\'
)
{
*
fixed
++
=
*
ptr
;
}
else
{
/* backslash, check the next char */
ptr
++
;
/* if we're at the end, it's a multiline, so keep the backslash */
if
(
*
ptr
==
'\0'
)
{
*
is_multi
=
true
;
goto
done
;
}
if
((
esc
=
strchr
(
escapes
,
*
ptr
))
!=
NULL
)
{
*
fixed
++
=
escaped
[
esc
-
escapes
];
}
else
{
git__free
(
str
);
giterr_set
(
GITERR_CONFIG
,
"Invalid escape at %s"
,
ptr
);
return
-
1
;
}
}
ptr
++
;
}
done:
*
fixed
=
'\0'
;
*
out
=
str
;
return
0
;
}
static
int
parse_multiline_variable
(
struct
reader
*
reader
,
git_buf
*
value
,
int
in_quotes
)
{
char
*
line
=
NULL
,
*
proc_line
=
NULL
;
int
quote_count
;
bool
multiline
;
/* Check that the next line exists */
line
=
reader_readline
(
reader
,
false
);
if
(
line
==
NULL
)
return
-
1
;
git__free
(
var_name
);
git__free
(
var_value
);
/*
We've reached the end of the file, there is no continuation.
*
(this is not an error)
.
/*
If this isn't the name/value we're looking for, simply dump the
*
existing data back out and continue on
.
*/
if
(
line
[
0
]
==
'\0'
)
{
git__free
(
line
);
return
0
;
}
quote_count
=
strip_comments
(
line
,
!!
in_quotes
);
/* If it was just a comment, pretend it didn't exist */
if
(
line
[
0
]
==
'\0'
)
{
git__free
(
line
);
return
parse_multiline_variable
(
reader
,
value
,
quote_count
);
/* TODO: unbounded recursion. This **could** be exploitable */
}
if
(
unescape_line
(
&
proc_line
,
&
multiline
,
line
,
in_quotes
)
<
0
)
{
git__free
(
line
);
return
-
1
;
}
/* add this line to the multiline var */
git_buf_puts
(
value
,
proc_line
);
git__free
(
line
);
git__free
(
proc_line
);
if
(
!
has_matched
)
return
write_line
(
write_data
,
line
,
line_len
);
/*
* If we need to continue reading the next line, let's just
* keep putting stuff in the buffer
*/
if
(
multiline
)
return
parse_multiline_variable
(
reader
,
value
,
quote_count
);
write_data
->
preg_replaced
=
1
;
/* If value is NULL, we are deleting this value; write nothing. */
if
(
!
write_data
->
value
)
return
0
;
}
GIT_INLINE
(
bool
)
is_namechar
(
char
c
)
{
return
isalnum
(
c
)
||
c
==
'-'
;
return
write_value
(
write_data
);
}
static
int
parse_name
(
char
**
name
,
const
char
**
value
,
struct
reader
*
reader
,
const
char
*
line
)
static
int
write_on_comment
(
struct
reader
**
reader
,
const
char
*
line
,
size_t
line_len
,
void
*
data
)
{
const
char
*
name_end
=
line
,
*
value_start
;
struct
write_data
*
write_data
;
*
name
=
NULL
;
*
value
=
NULL
;
GIT_UNUSED
(
reader
);
while
(
*
name_end
&&
is_namechar
(
*
name_end
))
name_end
++
;
write_data
=
(
struct
write_data
*
)
data
;
return
write_line
(
write_data
,
line
,
line_len
);
}
if
(
line
==
name_end
)
{
set_parse_error
(
reader
,
0
,
"Invalid configuration key"
);
return
-
1
;
}
static
int
write_on_eof
(
struct
reader
**
reader
,
void
*
data
)
{
struct
write_data
*
write_data
=
(
struct
write_data
*
)
data
;
int
result
=
0
;
value_start
=
name_end
;
GIT_UNUSED
(
reader
)
;
while
(
*
value_start
&&
git__isspace
(
*
value_start
))
value_start
++
;
if
(
*
value_start
==
'='
)
{
*
value
=
value_start
+
1
;
}
else
if
(
*
value_start
)
{
set_parse_error
(
reader
,
0
,
"Invalid configuration key"
);
return
-
1
;
/* If we are at the EOF and have not written our value (again, for a
* simple name/value set, not a multivar) then we have never seen the
* section in question and should create a new section and write the
* value.
*/
if
((
!
write_data
->
preg
||
!
write_data
->
preg_replaced
)
&&
write_data
->
value
)
{
if
((
result
=
write_section
(
write_data
->
file
,
write_data
->
section
))
==
0
)
result
=
write_value
(
write_data
)
;
}
if
((
*
name
=
git__strndup
(
line
,
name_end
-
line
))
==
NULL
)
return
-
1
;
return
0
;
return
result
;
}
static
int
parse_variable
(
struct
reader
*
reader
,
char
**
var_name
,
char
**
var_value
)
/*
* This is pretty much the parsing, except we write out anything we don't have
*/
static
int
config_write
(
diskfile_backend
*
cfg
,
const
char
*
key
,
const
regex_t
*
preg
,
const
char
*
value
)
{
const
char
*
value_start
=
NULL
;
char
*
line
;
int
quote_count
;
bool
multiline
;
line
=
reader_readline
(
reader
,
true
);
if
(
line
==
NULL
)
return
-
1
;
quote_count
=
strip_comments
(
line
,
0
);
int
result
;
char
*
section
,
*
name
,
*
ldot
;
git_filebuf
file
=
GIT_FILEBUF_INIT
;
struct
reader
*
reader
=
git_array_get
(
cfg
->
readers
,
0
);
struct
write_data
write_data
;
if
(
parse_name
(
var_name
,
&
value_start
,
reader
,
line
)
<
0
)
return
-
1
;
/* Lock the file */
if
((
result
=
git_filebuf_open
(
&
file
,
cfg
->
file_path
,
0
,
GIT_CONFIG_FILE_MODE
))
<
0
)
{
git_buf_free
(
&
reader
->
buffer
);
return
result
;
}
/*
If there is no value, boolean true is assumed
*/
*
var_value
=
NULL
;
/*
We need to read in our own config file
*/
result
=
git_futils_readbuffer
(
&
reader
->
buffer
,
cfg
->
file_path
)
;
/*
* Now, let's try to parse the value
*/
if
(
value_start
!=
NULL
)
{
while
(
git__isspace
(
value_start
[
0
]))
value_start
++
;
/* Initialise the reading position */
if
(
result
==
GIT_ENOTFOUND
)
{
reader
->
read_ptr
=
NULL
;
reader
->
eof
=
1
;
git_buf_clear
(
&
reader
->
buffer
);
}
else
if
(
result
==
0
)
{
reader
->
read_ptr
=
reader
->
buffer
.
ptr
;
reader
->
eof
=
0
;
}
else
{
git_filebuf_cleanup
(
&
file
);
return
-
1
;
/* OS error when reading the file */
}
if
(
unescape_line
(
var_value
,
&
multiline
,
value_start
,
0
)
<
0
)
goto
on_error
;
ldot
=
strrchr
(
key
,
'.'
);
name
=
ldot
+
1
;
section
=
git__strndup
(
key
,
ldot
-
key
);
if
(
multiline
)
{
git_buf
multi_value
=
GIT_BUF_INIT
;
git_buf_attach
(
&
multi_value
,
*
var_value
,
0
);
write_data
.
file
=
&
file
;
write_data
.
section
=
section
;
write_data
.
in_section
=
0
;
write_data
.
preg_replaced
=
0
;
write_data
.
name
=
name
;
write_data
.
preg
=
preg
;
write_data
.
value
=
value
;
if
(
parse_multiline_variable
(
reader
,
&
multi_value
,
quote_count
)
<
0
||
git_buf_oom
(
&
multi_value
))
{
git_buf_free
(
&
multi_value
);
goto
on_error
;
if
((
result
=
config_parse
(
reader
,
write_on_section
,
write_on_variable
,
write_on_comment
,
write_on_eof
,
&
write_data
))
<
0
)
{
git_filebuf_cleanup
(
&
file
);
goto
done
;
}
*
var_value
=
git_buf_detach
(
&
multi_value
);
}
}
/* refresh stats - if this errors, then commit will error too */
(
void
)
git_filebuf_stats
(
&
reader
->
file_mtime
,
&
reader
->
file_size
,
&
file
);
git__free
(
lin
e
);
return
0
;
result
=
git_filebuf_commit
(
&
fil
e
);
git_buf_free
(
&
reader
->
buffer
)
;
on_error
:
git__free
(
*
var_name
);
git__free
(
line
);
return
-
1
;
done:
git_buf_free
(
&
reader
->
buffer
);
return
result
;
}
tests/config/write.c
View file @
074d323f
...
...
@@ -145,6 +145,138 @@ void test_config_write__delete_value_with_duplicate_header(void)
git_config_free
(
cfg
);
}
/*
* This test exposes a bug where duplicate section headers could cause
* config_write to add a new entry when one already exists.
*/
void
test_config_write__add_value_with_duplicate_header
(
void
)
{
const
char
*
file_name
=
"config-duplicate-insert"
;
const
char
*
entry_name
=
"foo.c"
;
const
char
*
old_val
=
"old"
;
const
char
*
new_val
=
"new"
;
const
char
*
str
;
git_config
*
cfg
,
*
snapshot
;
/* c = old should be replaced by c = new.
* The bug causes c = new to be inserted under the first 'foo' header.
*/
const
char
*
file_content
=
"[foo]
\n
"
\
" a = b
\n
"
\
"[other]
\n
"
\
" a = b
\n
"
\
"[foo]
\n
"
\
" c = old
\n
"
;
/* Write the test config */
cl_git_mkfile
(
file_name
,
file_content
);
cl_git_pass
(
git_config_open_ondisk
(
&
cfg
,
file_name
));
/* make sure the expected entry (foo.c) exists */
cl_git_pass
(
git_config_snapshot
(
&
snapshot
,
cfg
));
cl_git_pass
(
git_config_get_string
(
&
str
,
snapshot
,
entry_name
));
cl_assert_equal_s
(
old_val
,
str
);
git_config_free
(
snapshot
);
/* Try setting foo.c to something else */
cl_git_pass
(
git_config_set_string
(
cfg
,
entry_name
,
new_val
));
git_config_free
(
cfg
);
/* Reopen the file and make sure the new value was set */
cl_git_pass
(
git_config_open_ondisk
(
&
cfg
,
file_name
));
cl_git_pass
(
git_config_snapshot
(
&
snapshot
,
cfg
));
cl_git_pass
(
git_config_get_string
(
&
str
,
snapshot
,
entry_name
));
cl_assert_equal_s
(
new_val
,
str
);
/* Cleanup */
git_config_free
(
snapshot
);
git_config_free
(
cfg
);
}
void
test_config_write__overwrite_value_with_duplicate_header
(
void
)
{
const
char
*
file_name
=
"config-duplicate-header"
;
const
char
*
entry_name
=
"remote.origin.url"
;
git_config
*
cfg
;
git_config_entry
*
entry
;
/* This config can occur after removing and re-adding the origin remote */
const
char
*
file_content
=
"[remote
\"
origin
\"
]
\n
"
\
"[branch
\"
master
\"
]
\n
"
\
" remote =
\"
origin
\"\n
"
\
"[remote
\"
origin
\"
]
\n
"
\
" url =
\"
foo
\"\n
"
;
/* Write the test config and make sure the expected entry exists */
cl_git_mkfile
(
file_name
,
file_content
);
cl_git_pass
(
git_config_open_ondisk
(
&
cfg
,
file_name
));
cl_git_pass
(
git_config_get_entry
(
&
entry
,
cfg
,
entry_name
));
/* Update that entry */
cl_git_pass
(
git_config_set_string
(
cfg
,
entry_name
,
"newurl"
));
/* Reopen the file and make sure the entry was updated */
git_config_entry_free
(
entry
);
git_config_free
(
cfg
);
cl_git_pass
(
git_config_open_ondisk
(
&
cfg
,
file_name
));
cl_git_pass
(
git_config_get_entry
(
&
entry
,
cfg
,
entry_name
));
cl_assert_equal_s
(
"newurl"
,
entry
->
value
);
/* Cleanup */
git_config_entry_free
(
entry
);
git_config_free
(
cfg
);
}
static
int
multivar_cb
(
const
git_config_entry
*
entry
,
void
*
data
)
{
int
*
n
=
(
int
*
)
data
;
cl_assert_equal_s
(
entry
->
value
,
"newurl"
);
(
*
n
)
++
;
return
0
;
}
void
test_config_write__overwrite_multivar_within_duplicate_header
(
void
)
{
const
char
*
file_name
=
"config-duplicate-header"
;
const
char
*
entry_name
=
"remote.origin.url"
;
git_config
*
cfg
;
git_config_entry
*
entry
;
int
n
=
0
;
/* This config can occur after removing and re-adding the origin remote */
const
char
*
file_content
=
"[remote
\"
origin
\"
]
\n
"
\
" url =
\"
bar
\"\n
"
\
"[branch
\"
master
\"
]
\n
"
\
" remote =
\"
origin
\"\n
"
\
"[remote
\"
origin
\"
]
\n
"
\
" url =
\"
foo
\"\n
"
;
/* Write the test config and make sure the expected entry exists */
cl_git_mkfile
(
file_name
,
file_content
);
cl_git_pass
(
git_config_open_ondisk
(
&
cfg
,
file_name
));
cl_git_pass
(
git_config_get_entry
(
&
entry
,
cfg
,
entry_name
));
/* Update that entry */
cl_git_pass
(
git_config_set_multivar
(
cfg
,
entry_name
,
".*"
,
"newurl"
));
git_config_entry_free
(
entry
);
git_config_free
(
cfg
);
/* Reopen the file and make sure the entry was updated */
cl_git_pass
(
git_config_open_ondisk
(
&
cfg
,
file_name
));
cl_git_pass
(
git_config_get_multivar_foreach
(
cfg
,
entry_name
,
NULL
,
multivar_cb
,
&
n
));
cl_assert_equal_i
(
2
,
n
);
/* Cleanup */
git_config_free
(
cfg
);
}
void
test_config_write__write_subsection
(
void
)
{
git_config
*
cfg
;
...
...
@@ -395,6 +527,75 @@ void test_config_write__outside_change(void)
git_config_free
(
cfg
);
}
#define SECTION_FOO \
"\n" \
" \n" \
" [section \"foo\"] \n" \
" # here's a comment\n" \
"\tname = \"value\"\n" \
" name2 = \"value2\"\n" \
"; another comment!\n"
#define SECTION_BAR \
"[section \"bar\"]\t\n" \
"\t \n" \
" barname=\"value\"\n"
void
test_config_write__preserves_whitespace_and_comments
(
void
)
{
const
char
*
file_name
=
"config-duplicate-header"
;
const
char
*
n
;
git_config
*
cfg
;
git_buf
newfile
=
GIT_BUF_INIT
;
/* This config can occur after removing and re-adding the origin remote */
const
char
*
file_content
=
SECTION_FOO
SECTION_BAR
;
/* Write the test config and make sure the expected entry exists */
cl_git_mkfile
(
file_name
,
file_content
);
cl_git_pass
(
git_config_open_ondisk
(
&
cfg
,
file_name
));
cl_git_pass
(
git_config_set_string
(
cfg
,
"section.foo.other"
,
"otherval"
));
cl_git_pass
(
git_config_set_string
(
cfg
,
"newsection.newname"
,
"new_value"
));
/* Ensure that we didn't needlessly mangle the config file */
cl_git_pass
(
git_futils_readbuffer
(
&
newfile
,
file_name
));
n
=
newfile
.
ptr
;
cl_assert_equal_strn
(
SECTION_FOO
,
n
,
strlen
(
SECTION_FOO
));
n
+=
strlen
(
SECTION_FOO
);
cl_assert_equal_strn
(
"
\t
other = otherval
\n
"
,
n
,
strlen
(
"
\t
other = otherval
\n
"
));
n
+=
strlen
(
"
\t
other = otherval
\n
"
);
cl_assert_equal_strn
(
SECTION_BAR
,
n
,
strlen
(
SECTION_BAR
));
n
+=
strlen
(
SECTION_BAR
);
cl_assert_equal_s
(
"[newsection]
\n\t
newname = new_value
\n
"
,
n
);
git_buf_free
(
&
newfile
);
git_config_free
(
cfg
);
}
void
test_config_write__preserves_entry_with_name_only
(
void
)
{
const
char
*
file_name
=
"config-empty-value"
;
git_config
*
cfg
;
git_buf
newfile
=
GIT_BUF_INIT
;
/* Write the test config and make sure the expected entry exists */
cl_git_mkfile
(
file_name
,
"[section
\"
foo
\"
]
\n\t
name
\n
"
);
cl_git_pass
(
git_config_open_ondisk
(
&
cfg
,
file_name
));
cl_git_pass
(
git_config_set_string
(
cfg
,
"newsection.newname"
,
"new_value"
));
cl_git_pass
(
git_config_set_string
(
cfg
,
"section.foo.other"
,
"otherval"
));
cl_git_pass
(
git_futils_readbuffer
(
&
newfile
,
file_name
));
cl_assert_equal_s
(
"[section
\"
foo
\"
]
\n\t
name
\n\t
other = otherval
\n
[newsection]
\n\t
newname = new_value
\n
"
,
newfile
.
ptr
);
git_buf_free
(
&
newfile
);
git_config_free
(
cfg
);
}
void
test_config_write__to_empty_file
(
void
)
{
git_config
*
cfg
;
...
...
@@ -428,3 +629,4 @@ void test_config_write__to_file_with_only_comment(void)
git_buf_free
(
&
result
);
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment