Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.11.1
Comment thread
greymag marked this conversation as resolved.

* [Feature] `finish`: new `--squash` (`-s`) flag to squash all feature commits into a single commit when merging into develop. Useful for tasks like golden tests updates.
* **Fixed** [L10n] `from_xml`: false-positive "No parameters found" error for `one`/`two` plural variants when the target locale legitimately omits the count parameter (e.g. Arabic "كل يوم" / "كل يومين"). Other plural quantities (`zero`, `few`, `many`, `other`) are still validated as before.

## 1.11.0

* [Feature] `finish`: append issue references as a markdown link (`[#N](url/N)`) when `issue_url` is set in alex config; falls back to the plain `(#N)` suffix otherwise.
Expand Down
25 changes: 21 additions & 4 deletions lib/commands/feature/finish_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class FinishCommand extends FeatureCommandBase {
static const _argDemo = CmdArg('demo');
static const _argIssue = CmdArg('issue', abbr: 'i');
static const _argChangelog = CmdArg('changelog', abbr: 'c');
static const _argSquash = CmdArg('squash', abbr: 's');

FinishCommand()
: super(
Expand All @@ -37,6 +38,12 @@ class FinishCommand extends FeatureCommandBase {
'Optional, you can provide it in interactive mode. '
'Example: alex finish feature -${_argChangelog.abbr}"Some new feature"',
valueHelp: 'CHANGELOG',
)
..addFlag(
_argSquash.name,
abbr: _argSquash.abbr,
help: 'Squash all feature commits into a single commit when merging '
'into develop. Useful for tasks like golden tests updates.',
);
}

Expand All @@ -47,6 +54,7 @@ class FinishCommand extends FeatureCommandBase {
final isDemo = args.getBool(_argDemo);
var issueId = args.getInt(_argIssue);
final changelog = args.getString(_argChangelog);
final squash = args.getBool(_argSquash);

final console = this.console;
final gitConfig = config.git;
Expand Down Expand Up @@ -105,8 +113,15 @@ class FinishCommand extends FeatureCommandBase {

// TODO: Merge develop in remote feature branch if conflict

printVerbose('Merge feature branch in develop');
git.gitflowFeatureFinish(branchName, deleteBranch: false);
printVerbose(squash
? 'Squash merge feature branch in develop'
: 'Merge feature branch in develop');
git.gitflowFeatureFinish(
branchName,
deleteBranch: false,
squash: squash,
squashMessage: 'Feature #$issueId: ${branch.name}.\n\nBy alex.',
);

printVerbose('Add entry in changelog');
final changed = await _updateChangelog(
Expand All @@ -131,15 +146,16 @@ class FinishCommand extends FeatureCommandBase {

if (localCommit == commonCommit) {
printVerbose('Remove local feature branch');
git.branchDelete(localName);
// Squash merge does not mark the branch as merged in git.
git.branchDelete(localName, force: squash);
} else {
printVerbose('Local branch different from remote. '
'Do not delete $localName');
}
}

printVerbose('Remove feature branch');
git.branchDelete(branchName);
git.branchDelete(branchName, force: squash);

printVerbose('Merge develop in ${git.branchTest}');
git.mergeDevelopInTest();
Expand Down Expand Up @@ -262,6 +278,7 @@ Which section to add:
class _Branch {
final GitCommands git;
final String name;

// TODO: multiple remotes
final String? remoteName;
final String? localName;
Expand Down
51 changes: 46 additions & 5 deletions lib/src/git/git.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,20 @@ class GitCommands {
}

void gitflowFeatureFinish(String branchName,
{bool deleteBranch = true, bool failOnMergeConflict = false}) {
{bool deleteBranch = true,
bool failOnMergeConflict = false,
bool squash = false,
String? squashMessage}) {
checkout(branchDevelop);
merge(branchName, failOnMergeConflict: failOnMergeConflict);
if (deleteBranch) branchDelete(branchName);
if (squash) {
mergeSquash(branchName, squashMessage ?? "Merge branch '$branchName'",
failOnMergeConflict: failOnMergeConflict);
} else {
merge(branchName, failOnMergeConflict: failOnMergeConflict);
}
// Squash merge does not mark the branch as merged, so a force delete
// is required to remove it.
if (deleteBranch) branchDelete(branchName, force: squash);
}

void mergeDevelopInTest({String? remote, bool failOnMergeConflict = false}) {
Expand Down Expand Up @@ -196,14 +206,14 @@ class GitCommands {
return git("fetch $remote", "fetch $remote");
}

void branchDelete(String branch) {
void branchDelete(String branch, {bool force = false}) {
if (branch.startsWith(_branchRemotePrefix)) {
final parts = branch.split(_sep);
final remote = parts[1];
final remoteBranchName = parts.sublist(2).join(_sep);
branchDeleteFromRemote(remoteBranchName, remote);
} else {
git("branch -d $branch", "delete $branch");
git("branch ${force ? '-D' : '-d'} $branch", "delete $branch");
}
}

Expand Down Expand Up @@ -241,6 +251,37 @@ class GitCommands {
}
}

/// Merges [branch] into the current branch squashing all its commits,
/// then records the result as a single commit with the given [message].
void mergeSquash(String branch, String message,
{bool failOnMergeConflict = false}) {
try {
_git(["merge", "--squash", branch], "squash merge $branch");
} on RunException catch (e) {
if (!failOnMergeConflict &&
e.exitCode == 1 &&
(e.message?.contains(
'Automatic merge failed; fix conflicts and then commit the result.') ??
false)) {
print.info('alex will continue after merge would be resolved');
do {
sleep(const Duration(seconds: 1));
// Squash merge does not set MERGE_HEAD, so we wait until there are
// no unmerged paths left instead of relying on isInMerge().
} while (hasUnmergedPaths());
} else {
rethrow;
}
}

commit(message);
}

bool hasUnmergedPaths() {
final res = git('diff --name-only --diff-filter=U', 'check unmerged paths');
return res.isNotEmpty;
}

bool isInMerge() {
try {
_git(['merge', 'HEAD'], 'Check if in merge', printIfError: false);
Expand Down
27 changes: 18 additions & 9 deletions lib/src/l10n/exporters/arb_exporter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ArbExporter extends L10nExporter<ArbLocale> {
(baseMeta['placeholders'] as Map<String, dynamic>).keys.toSet();

if (value is L10nTextEntry) {
map[key] = _processText(key, parameters, value.text);
map[key] = _processText(key, parameters, parameters, value.text);
} else if (value is L10nPluralEntry) {
// Получаем заголовочную часть
const pluralPrefix = ',plural,';
Expand All @@ -61,7 +61,14 @@ class ArbExporter extends L10nExporter<ArbLocale> {
: parameters
.where((param) => baseVal.contains('{$param}'))
.toSet();
return _addPlural(val, key, allowed, value, attr);
// For "one"/"two" quantities the target locale may legitimately
// omit the count parameter (e.g. Arabic "كل يوم" / "كل يومين"),
// so we don't require parameters there even if the base variant
// uses them. Other quantities are still validated as usual.
final required = (attr == '=1' || attr == '=2')
? const <String>{}
: allowed;
return _addPlural(val, key, allowed, required, value, attr);
});
val.write('}');

Expand All @@ -85,26 +92,28 @@ class ArbExporter extends L10nExporter<ArbLocale> {
}

void _addPlural(StringBuffer res, String key, Set<String> allowedParams,
String? val, String attr) {
Set<String> requiredParams, String? val, String attr) {
if (val != null) {
res
..write(attr)
..write('{')
..write(_processText(key, allowedParams, val))
..write(_processText(key, allowedParams, requiredParams, val))
..write('}');
}
}

String _processText(String key, Set<String> allowed, String text) {
return clearWinLines(_validateParameters(key, allowed, text)
String _processText(
String key, Set<String> allowed, Set<String> required, String text) {
return clearWinLines(_validateParameters(key, allowed, required, text)
.replaceAll(r'\n', '\n')
.replaceAll(r'\r', '\r')
// escape single quotes
// https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#escaping-syntax
.replaceAll("'", "''"));
}

String _validateParameters(String key, Set<String> allowed, String text) {
String _validateParameters(
String key, Set<String> allowed, Set<String> required, String text) {
final params = _paramRegExp.allMatches(text).map((e) => e.group(1));

if (params.isNotEmpty) {
Expand All @@ -115,9 +124,9 @@ class ArbExporter extends L10nExporter<ArbLocale> {
}
}
} else {
if (allowed.isNotEmpty) {
if (required.isNotEmpty) {
throw Exception('[$locale] No parameters found for key "$key". '
'Expected: ${allowed.join(', ')}.');
'Expected: ${required.join(', ')}.');
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/src/version.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ homepage: https://github.com/innim/
repository: https://github.com/Innim/alex
issue_tracker: https://github.com/Innim/alex/issues

version: 1.11.0
version: 1.11.1

environment:
sdk: ">=3.0.0 <4.0.0"
Expand Down
Loading