[geany/geany] f1c98e: Merge pull request #481 from techee/go

Colomban Wendling git-noreply at xxxxx
Thu May 28 14:50:01 UTC 2015


Branch:      refs/heads/master
Author:      Colomban Wendling <ban at herbesfolles.org>
Committer:   Colomban Wendling <ban at herbesfolles.org>
Date:        Thu, 28 May 2015 14:50:01 UTC
Commit:      f1c98e5423bab3b159f1a6e9a7b37673752eeb30
             https://github.com/geany/geany/commit/f1c98e5423bab3b159f1a6e9a7b37673752eeb30

Log Message:
-----------
Merge pull request #481 from techee/go

Go ctags parser, Scintilla lexer and Geany support improvements

Closes #238.


Modified Paths:
--------------
    data/filetypes.go
    src/document.c
    src/editor.c
    src/symbols.c
    tagmanager/ctags/go.c
    tests/ctags/Makefile.am
    tests/ctags/return-types.go
    tests/ctags/return-types.go.tags
    tests/ctags/test.go
    tests/ctags/test.go.tags

Modified: data/filetypes.go
10 lines changed, 7 insertions(+), 3 deletions(-)
===================================================================
@@ -4,13 +4,17 @@
 
 [keywords]
 # all items must be in one line
-primary=break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var
-secondary=byte bool rune int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 float32 float64 complex64 complex128 uintptr string
+primary=break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var true false iota nil
+secondary=bool byte complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr
 
 # these are the Doxygen keywords
 docComment=a addindex addtogroup anchor arg attention author authors b brief bug c callergraph callgraph category cite class code cond copybrief copydetails copydoc copyright date def defgroup deprecated details dir dontinclude dot dotfile e else elseif em endcode endcond enddot endhtmlonly endif endinternal endlatexonly endlink endmanonly endmsc endrtfonly endverbatim endxmlonly enum example exception extends file fn headerfile hideinitializer htmlinclude htmlonly if ifnot image implements include includelineno ingroup interface internal invariant latexonly li line link mainpage manonly memberof msc mscfile n name namespace nosubgrouping note overload p package page par paragraph param post pre private privatesection property protected protectedsection protocol public publicsection ref related relatedalso relates relatesalso remark remarks result return returns retval rtfonly sa section see short showinitializer since skip skipline snippet struct subpage subsection subsubsection tableofcontents test throw throws todo tparam typedef union until var verbatim verbinclude version warning weakgroup xmlonly xrefitem
 
-[lexer_properties=C]
+[lexer_properties]
+lexer.cpp.backquoted.strings=1
+styling.within.preprocessor=1
+lexer.cpp.allow.dollars=0
+fold.preprocessor=0
 
 [settings]
 


Modified: src/document.c
1 lines changed, 1 insertions(+), 0 deletions(-)
===================================================================
@@ -2649,6 +2649,7 @@ void document_highlight_tags(GeanyDocument *doc)
 		case GEANY_FILETYPES_OBJECTIVEC:
 		case GEANY_FILETYPES_VALA:
 		case GEANY_FILETYPES_RUST:
+		case GEANY_FILETYPES_GO:
 		{
 
 			/* index of the keyword set in the Scintilla lexer, for


Modified: src/editor.c
7 lines changed, 4 insertions(+), 3 deletions(-)
===================================================================
@@ -1790,7 +1790,7 @@ static gboolean append_calltip(GString *str, const TMTag *tag, filetype_id ft_id
 	if (! tag->arglist)
 		return FALSE;
 
-	if (ft_id != GEANY_FILETYPES_PASCAL)
+	if (ft_id != GEANY_FILETYPES_PASCAL && ft_id != GEANY_FILETYPES_GO)
 	{	/* usual calltips: "retval tagname (arglist)" */
 		if (tag->var_type)
 		{
@@ -1815,14 +1815,15 @@ static gboolean append_calltip(GString *str, const TMTag *tag, filetype_id ft_id
 		g_string_append(str, tag->arglist);
 	}
 	else
-	{	/* special case Pascal calltips: "tagname (arglist) : retval" */
+	{	/* special case Pascal/Go calltips: "tagname (arglist) : retval"
+		 * (with ':' omitted for Go) */
 		g_string_append(str, tag->name);
 		g_string_append_c(str, ' ');
 		g_string_append(str, tag->arglist);
 
 		if (!EMPTY(tag->var_type))
 		{
-			g_string_append(str, " : ");
+			g_string_append(str, ft_id == GEANY_FILETYPES_PASCAL ? " : " : " ");
 			g_string_append(str, tag->var_type);
 		}
 	}


Modified: src/symbols.c
9 lines changed, 6 insertions(+), 3 deletions(-)
===================================================================
@@ -698,7 +698,7 @@ static void add_top_level_items(GeanyDocument *doc)
 				&(tv_iters.tag_variable), _("Variables"), "classviewer-var",
 				&(tv_iters.tag_macro), _("Macros"), "classviewer-macro",
 				&(tv_iters.tag_member), _("Methods"), "classviewer-member",
-				&(tv_iters.tag_other), _("Other"), "classviewer-other", NULL,
+				&(tv_iters.tag_other), _("Other"), "classviewer-other",
 				NULL);
 			break;
 		}
@@ -707,10 +707,13 @@ static void add_top_level_items(GeanyDocument *doc)
 			tag_list_add_groups(tag_store,
 				&(tv_iters.tag_namespace), _("Package"), "classviewer-namespace",
 				&(tv_iters.tag_function), _("Functions"), "classviewer-method",
-				&(tv_iters.tag_macro), _("Constants"), "classviewer-macro",
+				&(tv_iters.tag_interface), _("Interfaces"), "classviewer-struct",
+				&(tv_iters.tag_struct), _("Structs"), "classviewer-struct",
 				&(tv_iters.tag_type), _("Types"), "classviewer-struct",
+				&(tv_iters.tag_macro), _("Constants"), "classviewer-macro",
 				&(tv_iters.tag_variable), _("Variables"), "classviewer-var",
-				&(tv_iters.tag_other), _("Other"), "classviewer-other", NULL,
+				&(tv_iters.tag_member), _("Members"), "classviewer-member",
+				&(tv_iters.tag_other), _("Other"), "classviewer-other",
 				NULL);
 			break;
 		}


Modified: tagmanager/ctags/go.c
262 lines changed, 214 insertions(+), 48 deletions(-)
===================================================================
@@ -13,6 +13,7 @@
 /*
  *	 MACROS
  */
+#define MAX_SIGNATURE_LENGTH 512
 #define isType(token,t) (boolean) ((token)->type == (t))
 #define isKeyword(token,k) (boolean) ((token)->keyword == (k))
 
@@ -77,6 +78,7 @@ typedef struct sTokenInfo {
 
 static int Lang_go;
 static vString *scope;
+static vString *signature = NULL;
 
 typedef enum {
 	GOTAG_UNDEFINED = -1,
@@ -85,6 +87,9 @@ typedef enum {
 	GOTAG_CONST,
 	GOTAG_TYPE,
 	GOTAG_VAR,
+	GOTAG_STRUCT,
+	GOTAG_INTERFACE,
+	GOTAG_MEMBER
 } goKind;
 
 static kindOption GoKinds[] = {
@@ -92,7 +97,10 @@ static kindOption GoKinds[] = {
 	{TRUE, 'f', "function", "functions"},
 	{TRUE, 'c', "macro", "constants"},
 	{TRUE, 't', "typedef", "types"},
-	{TRUE, 'v', "variable", "variables"}
+	{TRUE, 'v', "variable", "variables"},
+	{TRUE, 's', "struct", "structs"},
+	{TRUE, 'i', "interface", "interfaces"},
+	{TRUE, 'm', "member", "struct members"}
 };
 
 static keywordDesc GoKeywordTable[] = {
@@ -149,6 +157,17 @@ static tokenInfo *newToken (void)
 	return token;
 }
 
+static tokenInfo *copyToken (tokenInfo *other)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+	token->type = other->type;
+	token->keyword = other->keyword;
+	token->string = vStringNewCopy (other->string);
+	token->lineNumber = other->lineNumber;
+	token->filePosition = other->filePosition;
+	return token;
+}
+
 static void deleteToken (tokenInfo * const token)
 {
 	if (token != NULL)
@@ -201,6 +220,8 @@ static void readToken (tokenInfo *const token)
 {
 	int c;
 	static tokenType lastTokenType = TOKEN_NONE;
+	boolean firstWhitespace = TRUE;
+	boolean whitespace;
 
 	token->type = TOKEN_NONE;
 	token->keyword = KEYWORD_NONE;
@@ -219,11 +240,16 @@ static void readToken (tokenInfo *const token)
 						  lastTokenType == TOKEN_CLOSE_CURLY ||
 						  lastTokenType == TOKEN_CLOSE_SQUARE))
 		{
-			token->type = TOKEN_SEMICOLON;
-			goto done;
+			c = ';';  // semicolon injection
+		}
+		whitespace = c == '\t'  ||  c == ' ' ||  c == '\r' || c == '\n';
+		if (signature && whitespace && firstWhitespace && vStringLength (signature) < MAX_SIGNATURE_LENGTH)
+		{
+			firstWhitespace = FALSE;
+			vStringPut(signature, ' ');
 		}
 	}
-	while (c == '\t'  ||  c == ' ' ||  c == '\r' || c == '\n');
+	while (whitespace);
 
 	switch (c)
 	{
@@ -354,71 +380,79 @@ static void readToken (tokenInfo *const token)
 			break;
 	}
 
-done:
+	if (signature && vStringLength (signature) < MAX_SIGNATURE_LENGTH)
+	{
+		if (token->type == TOKEN_LEFT_ARROW)
+			vStringCatS(signature, "<-");
+		else if (token->type == TOKEN_STRING)
+		{
+			// only struct member annotations can appear in function prototypes
+			// so only `` type strings are possible
+			vStringPut(signature, '`');
+			vStringCat(signature, token->string);
+			vStringPut(signature, '`');
+		}
+		else if (token->type == TOKEN_IDENTIFIER || token->type == TOKEN_KEYWORD)
+			vStringCat(signature, token->string);
+		else if (c != EOF)
+			vStringPut(signature, c);
+	}
+
 	lastTokenType = token->type;
 }
 
-static void skipToMatched (tokenInfo *const token)
+static boolean skipToMatchedNoRead (tokenInfo *const token)
 {
 	int nest_level = 0;
-	tokenType open_token;
+	tokenType open_token = token->type;
 	tokenType close_token;
 
-	switch (token->type)
+	switch (open_token)
 	{
 		case TOKEN_OPEN_PAREN:
-			open_token = TOKEN_OPEN_PAREN;
 			close_token = TOKEN_CLOSE_PAREN;
 			break;
 		case TOKEN_OPEN_CURLY:
-			open_token = TOKEN_OPEN_CURLY;
 			close_token = TOKEN_CLOSE_CURLY;
 			break;
 		case TOKEN_OPEN_SQUARE:
-			open_token = TOKEN_OPEN_SQUARE;
 			close_token = TOKEN_CLOSE_SQUARE;
 			break;
 		default:
-			return;
+			return FALSE;
 	}
 
 	/*
 	 * This routine will skip to a matching closing token.
-	 * It will also handle nested tokens like the (, ) below.
-	 *   (  name varchar(30), text binary(10)  )
+	 * It will also handle nested tokens.
 	 */
-	if (isType (token, open_token))
+	nest_level++;
+	while (nest_level > 0 && !isType (token, TOKEN_EOF))
 	{
-		nest_level++;
-		while (!(isType (token, close_token) && (nest_level == 0)) &&
-			   !isType (token, TOKEN_EOF))
-		{
-			readToken (token);
-			if (isType (token, open_token))
-			{
-				nest_level++;
-			}
-			if (isType (token, close_token))
-			{
-				if (nest_level > 0)
-				{
-					nest_level--;
-				}
-			}
-		}
 		readToken (token);
+		if (isType (token, open_token))
+			nest_level++;
+		else if (isType (token, close_token))
+			nest_level--;
 	}
+
+	return TRUE;
 }
 
-static void skipType (tokenInfo *const token)
+static void skipToMatched (tokenInfo *const token)
+{
+	if (skipToMatchedNoRead (token))
+		readToken (token);
+}
+
+static boolean skipType (tokenInfo *const token)
 {
-again:
 	// Type      = TypeName | TypeLit | "(" Type ")" .
 	// Skips also function multiple return values "(" Type {"," Type} ")"
 	if (isType (token, TOKEN_OPEN_PAREN))
 	{
 		skipToMatched (token);
-		return;
+		return TRUE;
 	}
 
 	// TypeName  = QualifiedIdent.
@@ -433,7 +467,7 @@ static void skipType (tokenInfo *const token)
 			if (isType (token, TOKEN_IDENTIFIER))
 				readToken (token);
 		}
-		return;
+		return TRUE;
 	}
 
 	// StructType     = "struct" "{" { FieldDecl ";" } "}"
@@ -443,7 +477,7 @@ static void skipType (tokenInfo *const token)
 		readToken (token);
 		// skip over "{}"
 		skipToMatched (token);
-		return;
+		return TRUE;
 	}
 
 	// ArrayType   = "[" ArrayLength "]" ElementType .
@@ -452,7 +486,7 @@ static void skipType (tokenInfo *const token)
 	if (isType (token, TOKEN_OPEN_SQUARE))
 	{
 		skipToMatched (token);
-		goto again;
+		return skipType (token);
 	}
 
 	// PointerType = "*" BaseType .
@@ -461,7 +495,7 @@ static void skipType (tokenInfo *const token)
 	if (isType (token, TOKEN_STAR) || isKeyword (token, KEYWORD_chan) || isType (token, TOKEN_LEFT_ARROW))
 	{
 		readToken (token);
-		goto again;
+		return skipType (token);
 	}
 
 	// MapType     = "map" "[" KeyType "]" ElementType .
@@ -471,7 +505,7 @@ static void skipType (tokenInfo *const token)
 		readToken (token);
 		// skip over "[]"
 		skipToMatched (token);
-		goto again;
+		return skipType (token);
 	}
 
 	// FunctionType   = "func" Signature .
@@ -486,11 +520,15 @@ static void skipType (tokenInfo *const token)
 		// Result is parameters or type or nothing.  skipType treats anything
 		// surrounded by parentheses as a type, and does nothing if what
 		// follows is not a type.
-		goto again;
+		return skipType (token);
 	}
+
+	return FALSE;
 }
 
-static void makeTag (tokenInfo *const token, const goKind kind)
+static void makeTag (tokenInfo *const token, const goKind kind,
+	tokenInfo *const parent_token, const goKind parent_kind,
+	const char *argList, const char *varType)
 {
 	const char *const name = vStringValue (token->string);
 
@@ -504,7 +542,16 @@ static void makeTag (tokenInfo *const token, const goKind kind)
 	e.filePosition = token->filePosition;
 	e.kindName = GoKinds [kind].name;
 	e.kind = GoKinds [kind].letter;
+	if (argList)
+		e.extensionFields.arglist = argList;
+	if (varType)
+		e.extensionFields.varType = varType;
 
+	if (parent_kind != GOTAG_UNDEFINED && parent_token != NULL)
+	{
+		e.extensionFields.scope[0] = GoKinds[parent_kind].name;
+		e.extensionFields.scope[1] = vStringValue (parent_token->string);
+	}
 	makeTagEntry (&e);
 
 	if (scope && Option.include.qualifiedTags)
@@ -524,7 +571,7 @@ static void parsePackage (tokenInfo *const token)
 	readToken (token);
 	if (isType (token, TOKEN_IDENTIFIER))
 	{
-		makeTag (token, GOTAG_PACKAGE);
+		makeTag (token, GOTAG_PACKAGE, NULL, GOTAG_UNDEFINED, NULL, NULL);
 		if (!scope && Option.include.qualifiedTags)
 		{
 			scope = vStringNew ();
@@ -549,21 +596,115 @@ static void parseFunctionOrMethod (tokenInfo *const token)
 
 	if (isType (token, TOKEN_IDENTIFIER))
 	{
-		makeTag (token, GOTAG_FUNCTION);
-		
+		vString *argList;
+		tokenInfo *functionToken = copyToken (token);
+
+		// Start recording signature
+		signature = vStringNew ();
+
 		// Skip over parameters.
 		readToken (token);
-		skipToMatched (token);
+		skipToMatchedNoRead (token);
+
+		vStringStripLeading (signature);
+		vStringStripTrailing (signature);
+		argList = signature;
+		signature = vStringNew ();
+
+		readToken (token);
 
 		// Skip over result.
 		skipType (token);
 
+		// Remove the extra { we have just read
+		vStringStripTrailing (signature);
+		vStringChop (signature);
+
+		vStringStripLeading (signature);
+		vStringStripTrailing (signature);
+		makeTag (functionToken, GOTAG_FUNCTION, NULL, GOTAG_UNDEFINED, argList->buffer, signature->buffer);
+		deleteToken (functionToken);
+		vStringDelete(signature);
+		vStringDelete(argList);
+
+		// Stop recording signature
+		signature = NULL;
+
 		// Skip over function body.
 		if (isType (token, TOKEN_OPEN_CURLY))
 			skipToMatched (token);
 	}
 }
 
+static void parseStructMembers (tokenInfo *const token, tokenInfo *const parent_token)
+{
+	// StructType     = "struct" "{" { FieldDecl ";" } "}" .
+	// FieldDecl      = (IdentifierList Type | AnonymousField) [ Tag ] .
+	// AnonymousField = [ "*" ] TypeName .
+	// Tag            = string_lit .
+
+	readToken (token);
+	if (!isType (token, TOKEN_OPEN_CURLY))
+		return;
+
+	readToken (token);
+	while (!isType (token, TOKEN_EOF) && !isType (token, TOKEN_CLOSE_CURLY))
+	{
+		tokenInfo *memberCandidate = NULL;
+		boolean first = TRUE;
+
+		while (!isType (token, TOKEN_EOF))
+		{
+			if (isType (token, TOKEN_IDENTIFIER))
+			{
+				if (first)
+				{
+					// could be anonymous field like in 'struct {int}' - we don't know yet
+					memberCandidate = copyToken (token);
+					first = FALSE;
+				}
+				else
+				{
+					if (memberCandidate)
+					{
+						// if we are here, there was a comma and memberCandidate isn't an anonymous field
+						makeTag (memberCandidate, GOTAG_MEMBER, parent_token, GOTAG_STRUCT, NULL, NULL);
+						deleteToken (memberCandidate);
+						memberCandidate = NULL;
+					}
+					makeTag (token, GOTAG_MEMBER, parent_token, GOTAG_STRUCT, NULL, NULL);
+				}
+				readToken (token);
+			}
+			if (!isType (token, TOKEN_COMMA))
+				break;
+			readToken (token);
+		}
+
+		// in the case of  an anonymous field, we already read part of the
+		// type into memberCandidate and skipType() should return FALSE so no tag should
+		// be generated in this case.
+		if (skipType (token) && memberCandidate)
+			makeTag (memberCandidate, GOTAG_MEMBER, parent_token, GOTAG_STRUCT, NULL, NULL);
+
+		if (memberCandidate)
+			deleteToken (memberCandidate);
+
+		while (!isType (token, TOKEN_SEMICOLON) && !isType (token, TOKEN_CLOSE_CURLY)
+				&& !isType (token, TOKEN_EOF))
+		{
+			readToken (token);
+			skipToMatched (token);
+		}
+
+		if (!isType (token, TOKEN_CLOSE_CURLY))
+		{
+			// we are at TOKEN_SEMICOLON
+			readToken (token);
+		}
+	}
+}
+
 static void parseConstTypeVar (tokenInfo *const token, goKind kind)
 {
 	// ConstDecl      = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
@@ -586,11 +727,26 @@ static void parseConstTypeVar (tokenInfo *const token, goKind kind)
 
 	do
 	{
+		tokenInfo *typeToken = NULL;
+
 		while (!isType (token, TOKEN_EOF))
 		{
 			if (isType (token, TOKEN_IDENTIFIER))
 			{
-				makeTag (token, kind);
+				if (kind == GOTAG_TYPE)
+				{
+					typeToken = copyToken (token);
+					readToken (token);
+					if (isKeyword (token, KEYWORD_struct))
+						makeTag (typeToken, GOTAG_STRUCT, NULL, GOTAG_UNDEFINED, NULL, NULL);
+					else if (isKeyword (token, KEYWORD_interface))
+						makeTag (typeToken, GOTAG_INTERFACE, NULL, GOTAG_UNDEFINED, NULL, NULL);
+					else
+						makeTag (typeToken, kind, NULL, GOTAG_UNDEFINED, NULL, NULL);
+					break;
+				}
+				else
+					makeTag (token, kind, NULL, GOTAG_UNDEFINED, NULL, NULL);
 				readToken (token);
 			}
 			if (!isType (token, TOKEN_COMMA))
@@ -598,7 +754,17 @@ static void parseConstTypeVar (tokenInfo *const token, goKind kind)
 			readToken (token);
 		}
 
-		skipType (token);
+		if (typeToken)
+		{
+			if (isKeyword (token, KEYWORD_struct))
+				parseStructMembers (token, typeToken);
+			else
+				skipType (token);
+			deleteToken (typeToken);
+		}
+		else
+			skipType (token);
+
 		while (!isType (token, TOKEN_SEMICOLON) && !isType (token, TOKEN_CLOSE_PAREN)
 				&& !isType (token, TOKEN_EOF))
 		{


Modified: tests/ctags/Makefile.am
1 lines changed, 1 insertions(+), 0 deletions(-)
===================================================================
@@ -243,6 +243,7 @@ test_sources = \
 	refcurs.sql						\
 	regexp.js						\
 	return-hint.zep					\
+	return-types.go					\
 	rules.t2t						\
 	sample.t2t						\
 	secondary_fcn_name.js			\


Modified: tests/ctags/return-types.go
16 lines changed, 16 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,16 @@
+package main
+
+func foo() *struct {a int; b func()} {
+	return nil
+}
+
+func bar() (a int, b func(a int), c interface{}, d string) {
+	return 0, nil, nil, ""
+}
+
+func baz() *[20*3+1]map[chan<- /* map[int]int */<-chan int]map[interface{}]*string {
+	return nil
+}
+
+func main() {
+}
\ No newline at end of file


Modified: tests/ctags/return-types.go.tags
6 lines changed, 6 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,6 @@
+# format=tagmanager
+bar�16�()�0�(a int, b func(a int), c interface{}, d string)
+baz�16�()�0�*[20*3+1]map[chan<- <-chan int]map[interface{}]*string
+foo�16�()�0�*struct {a int; b func()}
+main�16�()�0�
+main�256�0


Modified: tests/ctags/test.go
9 lines changed, 6 insertions(+), 3 deletions(-)
===================================================================
@@ -15,8 +15,11 @@ type (
 )
 
 type T6 struct {
-	a, b, c, d int
-	e float32
+	_a, _b, _c, _d int
+	int
+	T1 `annotation`
+	*T2
+	_e float32
 	//ignored int
 }
 
@@ -44,4 +47,4 @@ func (tt * T7) f4(a func () func ()) (func (), int) {return func (){}, 1};func f
 func main() {
 	go func (){}()
 	fmt.Println("Hello, 世界")
-}
+}
\ No newline at end of file


Modified: tests/ctags/test.go.tags
21 lines changed, 13 insertions(+), 8 deletions(-)
===================================================================
@@ -12,23 +12,28 @@ T1
 T2�4096�0
 T3�4096�0
 T4�4096�0
-T5�4096�0
-T6�4096�0
+T5�32�0
+T6�2048�0
 T7�4096�0
 T8�4096�0
 T9�4096�0
+_a�64�T6�0
+_b�64�T6�0
+_c�64�T6�0
+_d�64�T6�0
+_e�64�T6�0
 a�16384�0
 b�16384�0
 c�16384�0
 d�16384�0
 e�16384�0
 f�16384�0
-f1�16�0
-f2�16�0
-f3�16�0
-f4�16�0
-f5�16�0
+f1�16�()�0�
+f2�16�()�0�
+f3�16�()�0�(a, b int)
+f4�16�(a func () func ())�0�(func (), int)
+f5�16�()�0�
 g�16384�0
 h�16384�0
-main�16�0
+main�16�()�0�
 main�256�0



--------------
This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).


More information about the Commits mailing list