Hi doug.gregor,
This patch adds new private headers to the module map. Private
headers may be included from within the module, but not from outside
the module.
The patch does not provide any symbol export control, only inclusion
control. This capability enables a small step towards limiting
unintended use of a module/library.
Changes are:
Add a list of PrivateHeaders to class Module. Private headers will
appear in both this list and the main Headers list.
Modify Module::print to print the new field.
Modify the ASTReader/ASTWriter to handle PrivateHeaders.
Concern: I am pretty sure I haven't quite gotten streaming right.
In ModuleMap, define an enum that describes the role of the header:
normal, excluded, or private.
Modify ModuleMap::addHeader to take that enum rather than a
boolean, and to also put the header on the PrivateHeaders list
when the role is private.
Concern: I have not modified KnownHeader to understand
PrivateHeaders. It wasn't necessary to this point, but I think
doing so is likely to improve performance. Thoughts?
In ModuleMap, add syntax for private headers. This syntax is the
same as excluded headers, but substituting the token "private"
instead of "exclude".
Add a new MMToken "private".
Change the parameters of parseHeaderDecl from two mutually
exclusive SourceLocations to a the leading token and the source
location of that token. Modify callers to match. Some checking
need not happen as a result. Infer the header role from the
leading token.
In PPDirectives, check that the module of the including file matches
the module of the included file.
Modify Preprocessor::LookupFile to track the SourceLocation of a
filename through its parameters to the diagnostic. Modify callers
to match.
Add new module tests for private headers. These are pretty minimal.
http://llvm-reviews.chandlerc.com/D584
Files:
lib/Serialization/ASTReader.cpp
lib/Serialization/ASTWriter.cpp
lib/Basic/Module.cpp
lib/Lex/PPDirectives.cpp
lib/Lex/Pragma.cpp
lib/Lex/ModuleMap.cpp
lib/Lex/PPMacroExpansion.cpp
test/Modules/Inputs/private2/common.h
test/Modules/Inputs/private1/private.h
test/Modules/Inputs/private1/module.map
test/Modules/Inputs/private1/public.h
test/Modules/private1.cpp
test/Modules/private2.cpp
test/Modules/private3.cpp
include/clang/Serialization/ASTBitCodes.h
include/clang/Basic/DiagnosticLexKinds.td
include/clang/Basic/Module.h
include/clang/Lex/ModuleMap.h
include/clang/Lex/Preprocessor.h
Index: lib/Serialization/ASTReader.cpp
===================================================================
--- lib/Serialization/ASTReader.cpp
+++ lib/Serialization/ASTReader.cpp
@@ -1287,7 +1287,9 @@
FileManager &FileMgr = Reader.getFileManager();
ModuleMap &ModMap =
Reader.getPreprocessor().getHeaderSearchInfo().getModuleMap();
- ModMap.addHeader(Mod, FileMgr.getFile(key.Filename), /*Excluded=*/false);
+ // FIXME: The following call fails to account for private headers.
+ ModMap.addHeader(Mod, FileMgr.getFile(key.Filename),
+ ModuleMap::NormalHeader);
}
}
@@ -3655,6 +3657,21 @@
break;
}
+ case SUBMODULE_PRIVATE_HEADER: {
+ if (First) {
+ Error("missing submodule metadata record at beginning of block");
+ return true;
+ }
+
+ if (!CurrentModule)
+ break;
+
+ // We lazily associate headers with their modules via the HeaderInfoTable.
+ // FIXME: Re-evaluate this section; maybe only store InputFile IDs instead
+ // of complete filenames or remove it entirely.
+ break;
+ }
+
case SUBMODULE_TOPHEADER: {
if (First) {
Error("missing submodule metadata record at beginning of block");
Index: lib/Serialization/ASTWriter.cpp
===================================================================
--- lib/Serialization/ASTWriter.cpp
+++ lib/Serialization/ASTWriter.cpp
@@ -2256,6 +2256,11 @@
unsigned ExcludedHeaderAbbrev = Stream.EmitAbbrev(Abbrev);
Abbrev = new BitCodeAbbrev();
+ Abbrev->Add(BitCodeAbbrevOp(SUBMODULE_PRIVATE_HEADER));
+ Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Blob)); // Name
+ unsigned PrivateHeaderAbbrev = Stream.EmitAbbrev(Abbrev);
+
+ Abbrev = new BitCodeAbbrev();
Abbrev->Add(BitCodeAbbrevOp(SUBMODULE_LINK_LIBRARY));
Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Fixed, 1)); // IsFramework
Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Blob)); // Name
@@ -2341,6 +2346,13 @@
Stream.EmitRecordWithBlob(ExcludedHeaderAbbrev, Record,
Mod->ExcludedHeaders[I]->getName());
}
+ // Emit the private headers.
+ for (unsigned I = 0, N = Mod->PrivateHeaders.size(); I != N; ++I) {
+ Record.clear();
+ Record.push_back(SUBMODULE_PRIVATE_HEADER);
+ Stream.EmitRecordWithBlob(PrivateHeaderAbbrev, Record,
+ Mod->PrivateHeaders[I]->getName());
+ }
ArrayRef<const FileEntry *>
TopHeaders = Mod->getTopHeaders(PP->getFileManager());
for (unsigned I = 0, N = TopHeaders.size(); I != N; ++I) {
Index: lib/Basic/Module.cpp
===================================================================
--- lib/Basic/Module.cpp
+++ lib/Basic/Module.cpp
@@ -306,6 +306,13 @@
OS.write_escaped(ExcludedHeaders[I]->getName());
OS << "\"\n";
}
+
+ for (unsigned I = 0, N = PrivateHeaders.size(); I != N; ++I) {
+ OS.indent(Indent + 2);
+ OS << "private header \"";
+ OS.write_escaped(PrivateHeaders[I]->getName());
+ OS << "\"\n";
+ }
for (submodule_const_iterator MI = submodule_begin(), MIEnd = submodule_end();
MI != MIEnd; ++MI)
Index: lib/Lex/PPDirectives.cpp
===================================================================
--- lib/Lex/PPDirectives.cpp
+++ lib/Lex/PPDirectives.cpp
@@ -514,6 +514,7 @@
}
const FileEntry *Preprocessor::LookupFile(
+ SourceLocation FilenameLoc,
StringRef Filename,
bool isAngled,
const DirectoryLookup *FromDir,
@@ -546,7 +547,26 @@
const FileEntry *FE = HeaderInfo.LookupFile(
Filename, isAngled, FromDir, CurDir, CurFileEnt,
SearchPath, RelativePath, SuggestedModule, SkipCache);
- if (FE) return FE;
+ if (FE) {
+ if (SuggestedModule) {
+ Module *RequestedModule = *SuggestedModule;
+ if (RequestedModule) {
+ // FIXME: This code is inefficient. But at present,
+ // not enough information is coming back with the module.
+ SmallVectorImpl<const FileEntry *> &PvtHdrs
+ = RequestedModule->PrivateHeaders;
+ SmallVectorImpl<const FileEntry *>::iterator Look
+ = std::find(PvtHdrs.begin(), PvtHdrs.end(), FE);
+ bool IsPrivate = Look != PvtHdrs.end();
+ if (IsPrivate) {
+ Module *IncludingModule = HeaderInfo.findModuleForHeader(CurFileEnt);
+ if (IncludingModule != *SuggestedModule)
+ Diag(FilenameLoc, diag::error_use_of_private_header_outside_module);
+ }
+ }
+ }
+ return FE;
+ }
// Otherwise, see if this is a subframework header. If so, this is relative
// to one of the headers on the #include stack. Walk the list of the current
@@ -1381,8 +1401,9 @@
// We get the raw path only if we have 'Callbacks' to which we later pass
// the path.
Module *SuggestedModule = 0;
+ SourceLocation FilenameLoc = FilenameTok.getLocation();
const FileEntry *File = LookupFile(
- Filename, isAngled, LookupFrom, CurDir,
+ FilenameLoc, Filename, isAngled, LookupFrom, CurDir,
Callbacks ? &SearchPath : NULL, Callbacks ? &RelativePath : NULL,
getLangOpts().Modules? &SuggestedModule : 0);
@@ -1397,8 +1418,8 @@
HeaderInfo.AddSearchPath(DL, isAngled);
// Try the lookup again, skipping the cache.
- File = LookupFile(Filename, isAngled, LookupFrom, CurDir, 0, 0,
- getLangOpts().Modules? &SuggestedModule : 0,
+ File = LookupFile(FilenameLoc, Filename, isAngled, LookupFrom, CurDir,
+ 0, 0, getLangOpts().Modules? &SuggestedModule : 0,
/*SkipCache*/true);
}
}
@@ -1419,7 +1440,7 @@
// brackets, we can attempt a lookup as though it were a quoted path to
// provide the user with a possible fixit.
if (isAngled) {
- File = LookupFile(Filename, false, LookupFrom, CurDir,
+ File = LookupFile(FilenameLoc, Filename, false, LookupFrom, CurDir,
Callbacks ? &SearchPath : 0,
Callbacks ? &RelativePath : 0,
getLangOpts().Modules ? &SuggestedModule : 0);
Index: lib/Lex/Pragma.cpp
===================================================================
--- lib/Lex/Pragma.cpp
+++ lib/Lex/Pragma.cpp
@@ -464,8 +464,8 @@
// Search include directories for this file.
const DirectoryLookup *CurDir;
- const FileEntry *File = LookupFile(Filename, isAngled, 0, CurDir, NULL, NULL,
- NULL);
+ const FileEntry *File = LookupFile(FilenameTok.getLocation(), Filename,
+ isAngled, 0, CurDir, NULL, NULL, NULL);
if (File == 0) {
if (!SuppressIncludeNotFoundError)
Diag(FilenameTok, diag::err_pp_file_not_found) << Filename;
Index: lib/Lex/ModuleMap.cpp
===================================================================
--- lib/Lex/ModuleMap.cpp
+++ lib/Lex/ModuleMap.cpp
@@ -562,14 +562,16 @@
}
void ModuleMap::addHeader(Module *Mod, const FileEntry *Header,
- bool Excluded) {
- if (Excluded) {
+ ModuleHeaderRole Role) {
+ if (Role == ExcludedHeader) {
Mod->ExcludedHeaders.push_back(Header);
} else {
+ if (Role == PrivateHeader)
+ Mod->PrivateHeaders.push_back(Header);
Mod->Headers.push_back(Header);
HeaderInfo.MarkFileModuleHeader(Header);
}
- Headers[Header] = KnownHeader(Mod, Excluded);
+ Headers[Header] = KnownHeader(Mod, Role == ExcludedHeader);
}
const FileEntry *
@@ -681,6 +683,7 @@
LinkKeyword,
ModuleKeyword,
Period,
+ PrivateKeyword,
UmbrellaKeyword,
RequiresKeyword,
Star,
@@ -766,7 +769,8 @@
bool parseModuleId(ModuleId &Id);
void parseModuleDecl();
void parseRequiresDecl();
- void parseHeaderDecl(SourceLocation UmbrellaLoc, SourceLocation ExcludeLoc);
+ void parseHeaderDecl(clang::MMToken::TokenKind,
+ SourceLocation LeadingLoc);
void parseUmbrellaDirDecl(SourceLocation UmbrellaLoc);
void parseExportDecl();
void parseLinkDecl();
@@ -818,6 +822,7 @@
.Case("header", MMToken::HeaderKeyword)
.Case("link", MMToken::LinkKeyword)
.Case("module", MMToken::ModuleKeyword)
+ .Case("private", MMToken::PrivateKeyword)
.Case("requires", MMToken::RequiresKeyword)
.Case("umbrella", MMToken::UmbrellaKeyword)
.Default(MMToken::Identifier);
@@ -1157,25 +1162,36 @@
case MMToken::UmbrellaKeyword: {
SourceLocation UmbrellaLoc = consumeToken();
if (Tok.is(MMToken::HeaderKeyword))
- parseHeaderDecl(UmbrellaLoc, SourceLocation());
+ parseHeaderDecl(MMToken::UmbrellaKeyword, UmbrellaLoc);
else
parseUmbrellaDirDecl(UmbrellaLoc);
break;
}
case MMToken::ExcludeKeyword: {
SourceLocation ExcludeLoc = consumeToken();
if (Tok.is(MMToken::HeaderKeyword)) {
- parseHeaderDecl(SourceLocation(), ExcludeLoc);
+ parseHeaderDecl(MMToken::ExcludeKeyword, ExcludeLoc);
} else {
Diags.Report(Tok.getLocation(), diag::err_mmap_expected_header)
<< "exclude";
}
break;
}
+ case MMToken::PrivateKeyword: {
+ SourceLocation PrivateLoc = consumeToken();
+ if (Tok.is(MMToken::HeaderKeyword)) {
+ parseHeaderDecl(MMToken::PrivateKeyword, PrivateLoc);
+ } else {
+ Diags.Report(Tok.getLocation(), diag::err_mmap_expected_header)
+ << "private";
+ }
+ break;
+ }
+
case MMToken::HeaderKeyword:
- parseHeaderDecl(SourceLocation(), SourceLocation());
+ parseHeaderDecl(MMToken::HeaderKeyword, SourceLocation());
break;
case MMToken::LinkKeyword:
@@ -1289,14 +1305,11 @@
/// header-declaration:
/// 'umbrella'[opt] 'header' string-literal
/// 'exclude'[opt] 'header' string-literal
-void ModuleMapParser::parseHeaderDecl(SourceLocation UmbrellaLoc,
- SourceLocation ExcludeLoc) {
+void ModuleMapParser::parseHeaderDecl(MMToken::TokenKind LeadingToken,
+ SourceLocation LeadingLoc) {
assert(Tok.is(MMToken::HeaderKeyword));
consumeToken();
- bool Umbrella = UmbrellaLoc.isValid();
- bool Exclude = ExcludeLoc.isValid();
- assert(!(Umbrella && Exclude) && "Cannot have both 'umbrella' and 'exclude'");
// Parse the header name.
if (!Tok.is(MMToken::StringLiteral)) {
Diags.Report(Tok.getLocation(), diag::err_mmap_expected_header)
@@ -1308,7 +1321,7 @@
SourceLocation FileNameLoc = consumeToken();
// Check whether we already have an umbrella.
- if (Umbrella && ActiveModule->Umbrella) {
+ if (LeadingToken == MMToken::UmbrellaKeyword && ActiveModule->Umbrella) {
Diags.Report(FileNameLoc, diag::err_mmap_umbrella_clash)
<< ActiveModule->getFullModuleName();
HadError = true;
@@ -1354,8 +1367,9 @@
// If this is a system module with a top-level header, this header
// may have a counterpart (or replacement) in the set of headers
// supplied by Clang. Find that builtin header.
- if (ActiveModule->IsSystem && !Umbrella && BuiltinIncludeDir &&
- BuiltinIncludeDir != Directory && isBuiltinHeader(FileName)) {
+ if (ActiveModule->IsSystem && LeadingToken != MMToken::UmbrellaKeyword &&
+ BuiltinIncludeDir && BuiltinIncludeDir != Directory &&
+ isBuiltinHeader(FileName)) {
SmallString<128> BuiltinPathName(BuiltinIncludeDir->getName());
llvm::sys::path::append(BuiltinPathName, FileName);
BuiltinFile = SourceMgr.getFileManager().getFile(BuiltinPathName);
@@ -1378,29 +1392,37 @@
Diags.Report(FileNameLoc, diag::err_mmap_header_conflict)
<< FileName << OwningModule.getModule()->getFullModuleName();
HadError = true;
- } else if (Umbrella) {
+ } else if (LeadingToken == MMToken::UmbrellaKeyword) {
const DirectoryEntry *UmbrellaDir = File->getDir();
if (Module *UmbrellaModule = Map.UmbrellaDirs[UmbrellaDir]) {
- Diags.Report(UmbrellaLoc, diag::err_mmap_umbrella_clash)
+ Diags.Report(LeadingLoc, diag::err_mmap_umbrella_clash)
<< UmbrellaModule->getFullModuleName();
HadError = true;
} else {
// Record this umbrella header.
Map.setUmbrellaHeader(ActiveModule, File);
}
} else {
// Record this header.
- Map.addHeader(ActiveModule, File, Exclude);
+ ModuleMap::ModuleHeaderRole Role = ModuleMap::NormalHeader;
+ if (LeadingToken == MMToken::ExcludeKeyword)
+ Role = ModuleMap::ExcludedHeader;
+ else if (LeadingToken == MMToken::PrivateKeyword)
+ Role = ModuleMap::PrivateHeader;
+ else
+ assert(LeadingToken == MMToken::HeaderKeyword);
+
+ Map.addHeader(ActiveModule, File, Role);
// If there is a builtin counterpart to this file, add it now.
if (BuiltinFile)
- Map.addHeader(ActiveModule, BuiltinFile, Exclude);
+ Map.addHeader(ActiveModule, BuiltinFile, Role);
}
- } else if (!Exclude) {
+ } else if (LeadingToken != MMToken::ExcludeKeyword) {
// Ignore excluded header files. They're optional anyway.
Diags.Report(FileNameLoc, diag::err_mmap_header_not_found)
- << Umbrella << FileName;
+ << (LeadingToken == MMToken::UmbrellaKeyword) << FileName;
HadError = true;
}
}
@@ -1767,6 +1789,7 @@
case MMToken::ExplicitKeyword:
case MMToken::ModuleKeyword:
case MMToken::HeaderKeyword:
+ case MMToken::PrivateKeyword:
case MMToken::UmbrellaKeyword:
default:
Diags.Report(Tok.getLocation(), diag::err_mmap_expected_inferred_member)
@@ -1893,6 +1916,7 @@
case MMToken::LinkKeyword:
case MMToken::LSquare:
case MMToken::Period:
+ case MMToken::PrivateKeyword:
case MMToken::RBrace:
case MMToken::RSquare:
case MMToken::RequiresKeyword:
Index: lib/Lex/PPMacroExpansion.cpp
===================================================================
--- lib/Lex/PPMacroExpansion.cpp
+++ lib/Lex/PPMacroExpansion.cpp
@@ -1080,7 +1080,8 @@
// Search include directories.
const DirectoryLookup *CurDir;
const FileEntry *File =
- PP.LookupFile(Filename, isAngled, LookupFrom, CurDir, NULL, NULL, NULL);
+ PP.LookupFile(FilenameLoc, Filename, isAngled, LookupFrom, CurDir, NULL,
+ NULL, NULL);
// Get the result value. A result of true means the file exists.
return File != 0;
Index: test/Modules/Inputs/private2/common.h
===================================================================
--- test/Modules/Inputs/private2/common.h
+++ test/Modules/Inputs/private2/common.h
@@ -0,0 +1,6 @@
+#ifndef COMMON_H
+#define COMMON_H
+
+typedef int common;
+
+#endif
Index: test/Modules/Inputs/private1/private.h
===================================================================
--- test/Modules/Inputs/private1/private.h
+++ test/Modules/Inputs/private1/private.h
@@ -0,0 +1,9 @@
+#ifndef A1_H
+#define A1_H
+
+#include "common.h"
+
+struct mitts_off { common field; };
+struct mitts_off hidden_variable;
+
+#endif
Index: test/Modules/Inputs/private1/module.map
===================================================================
--- test/Modules/Inputs/private1/module.map
+++ test/Modules/Inputs/private1/module.map
@@ -0,0 +1,4 @@
+module libPrivate {
+ header "public.h"
+ private header "private.h"
+}
Index: test/Modules/Inputs/private1/public.h
===================================================================
--- test/Modules/Inputs/private1/public.h
+++ test/Modules/Inputs/private1/public.h
@@ -0,0 +1,9 @@
+#ifndef A_H
+#define A_H
+
+#include "private.h"
+
+struct use_this { struct mitts_off field; };
+struct use_this public_variable;
+
+#endif
Index: test/Modules/private1.cpp
===================================================================
--- test/Modules/private1.cpp
+++ test/Modules/private1.cpp
@@ -0,0 +1,8 @@
+// RUN: rm -rf %t
+// RUN: %clang_cc1 -x objective-c -fmodules-cache-path=%t -fmodules -I %S/Inputs/private1 -I %S/Inputs/private2 %s -verify
+// expected-no-diagnostics
+
+#include "common.h"
+@import libPrivate;
+
+struct use_this client_variable;
Index: test/Modules/private2.cpp
===================================================================
--- test/Modules/private2.cpp
+++ test/Modules/private2.cpp
@@ -0,0 +1,8 @@
+// RUN: rm -rf %t
+// RUN: %clang_cc1 -x objective-c -fmodules-cache-path=%t -fmodules -I %S/Inputs/private1 -I %S/Inputs/private2 %s -verify
+// expected-no-diagnostics
+
+#include "common.h"
+#include "public.h"
+
+struct use_this client_variable;
Index: test/Modules/private3.cpp
===================================================================
--- test/Modules/private3.cpp
+++ test/Modules/private3.cpp
@@ -0,0 +1,8 @@
+// RUN: rm -rf %t
+// RUN: %clang_cc1 -x objective-c -fmodules-cache-path=%t -fmodules -I %S/Inputs/private1 -I %S/Inputs/private2 %s -verify
+
+#include "common.h"
+#include "public.h"
+#include "private.h" // expected-error {{use of private header from outside its module}}
+
+struct use_this client_variable;
Index: include/clang/Serialization/ASTBitCodes.h
===================================================================
--- include/clang/Serialization/ASTBitCodes.h
+++ include/clang/Serialization/ASTBitCodes.h
@@ -623,7 +623,9 @@
/// \brief Specifies a configuration macro for this module.
SUBMODULE_CONFIG_MACRO = 11,
/// \brief Specifies a conflict with another module.
- SUBMODULE_CONFLICT = 12
+ SUBMODULE_CONFLICT = 12,
+ /// \brief Specifies a header that is private to this submodule.
+ SUBMODULE_PRIVATE_HEADER = 13
};
/// \brief Record types used within a comments block.
Index: include/clang/Basic/DiagnosticLexKinds.td
===================================================================
--- include/clang/Basic/DiagnosticLexKinds.td
+++ include/clang/Basic/DiagnosticLexKinds.td
@@ -579,5 +579,7 @@
InGroup<IncompleteUmbrella>;
def err_expected_id_building_module : Error<
"expected a module name in '__building_module' expression">;
+def error_use_of_private_header_outside_module : Error<
+ "use of private header from outside its module">;
}
Index: include/clang/Basic/Module.h
===================================================================
--- include/clang/Basic/Module.h
+++ include/clang/Basic/Module.h
@@ -82,6 +82,10 @@
/// \brief The headers that are explicitly excluded from this module.
SmallVector<const FileEntry *, 2> ExcludedHeaders;
+ /// \brief The headers that are private to this module.
+ /// They will also appear in Headers>
+ llvm::SmallVector<const FileEntry *, 2> PrivateHeaders;
+
/// \brief The set of language features required to use this module.
///
/// If any of these features is not present, the \c IsAvailable bit
Index: include/clang/Lex/ModuleMap.h
===================================================================
--- include/clang/Lex/ModuleMap.h
+++ include/clang/Lex/ModuleMap.h
@@ -306,10 +306,16 @@
/// directory.
void setUmbrellaDir(Module *Mod, const DirectoryEntry *UmbrellaDir);
+ /// \brief Describes the role of a module header.
+ enum ModuleHeaderRole { NormalHeader, PrivateHeader, ExcludedHeader };
+
/// \brief Adds this header to the given module.
- /// \param Excluded Whether this header is explicitly excluded from the
- /// module; otherwise, it's included in the module.
- void addHeader(Module *Mod, const FileEntry *Header, bool Excluded);
+ /// \param Role
+ /// (NormalHeader) This header is normally included in the module.
+ /// (PrivateHeader) This header is included but private.
+ /// (ExcludedHeader) This header is explicitly excluded from the module.
+ void addHeader(Module *Mod, const FileEntry *Header,
+ ModuleHeaderRole Role);
/// \brief Parse the given module map file, and record any modules we
/// encounter.
Index: include/clang/Lex/Preprocessor.h
===================================================================
--- include/clang/Lex/Preprocessor.h
+++ include/clang/Lex/Preprocessor.h
@@ -1234,7 +1234,7 @@
///
/// Returns null on failure. \p isAngled indicates whether the file
/// reference is for system \#include's or not (i.e. using <> instead of "").
- const FileEntry *LookupFile(StringRef Filename,
+ const FileEntry *LookupFile(SourceLocation FilenameLoc, StringRef Filename,
bool isAngled, const DirectoryLookup *FromDir,
const DirectoryLookup *&CurDir,
SmallVectorImpl<char> *SearchPath,
_______________________________________________
cfe-commits mailing list
[email protected]
http://lists.cs.uiuc.edu/mailman/listinfo/cfe-commits