installGitattributes function

Future<void> installGitattributes({
  1. required Directory directory,
  2. required GgLog ggLog,
  3. SortedProcessingList? sortedProcessingList,
  4. ProcessRunner? processRunner,
})

Ensures a .gitattributes file containing all lines gitattributesRequiredLinesFor returns for the repository exists in every repository of the ticket directory lies in, and that the merge=ours driver is configured locally.

gg refuses to operate (e.g. gg do commit) when automatic EOL conversion is not configured via .gitattributes. In addition, the ticket workflow relies on merge=ours rules for state files and on merge=union for the changelog so that merging main into a feature branch does not produce textual conflicts. The referenced ours driver only works once git config merge.ours.driver true has been set in each repository; union is built into git.

Behaviour per repository:

  • If .gitattributes does not exist, it is created containing all required lines.
  • If .gitattributes exists, every required line that is missing is appended individually.
  • If all required lines are already present, the file is left untouched.
  • If a .git directory exists, git config merge.ours.driver true is executed with the repository as the working directory so the merge=ours rules can be honored by git.

This used to be the do install-gitattributes command. It is not exposed on the CLI any more — do add is the only caller and runs it for you.

Implementation

Future<void> installGitattributes({
  required Directory directory,
  required GgLog ggLog,
  SortedProcessingList? sortedProcessingList,
  ProcessRunner? processRunner,
}) async {
  final list = sortedProcessingList ?? SortedProcessingList(ggLog: ggLog);
  final runner = processRunner ?? ggRunProcess;

  // Detect ticket folder -----------------------------------------------------
  final String? ticketPath = WorkspaceUtils.detectTicketPath(
    path.absolute(directory.path),
  );
  if (ticketPath == null) {
    ggLog(cAction('Please run this command inside a ticket folder.'));
    throw Exception(cDetail('Not inside a ticket folder'));
  }

  final ticketDir = Directory(ticketPath);
  final ticketName = path.basename(ticketDir.path);

  // Collect all repositories in the ticket ----------------------------------
  final nodes = await list.get(directory: ticketDir, ggLog: ggLog);

  if (nodes.isEmpty) {
    ggLog(cWarn('⚠️ No repos in this ticket'));
    return;
  }

  // Ensure .gitattributes and merge.ours driver in each repository ----------
  for (final node in nodes) {
    final repoDir = node.directory;
    final repoName = path.basename(repoDir.path);

    final requiredLines = gitattributesRequiredLinesFor(repoDir.path);

    final attributesFile = File(path.join(repoDir.path, '.gitattributes'));

    if (!attributesFile.existsSync()) {
      await attributesFile.writeAsString('${requiredLines.join('\n')}\n');
      ggLog('Created .gitattributes in $repoName.');
    } else {
      final existing = await attributesFile.readAsString();
      final existingLines = const LineSplitter()
          .convert(existing)
          .map((l) => l.trim())
          .toSet();

      final missingLines = requiredLines
          .where((l) => !existingLines.contains(l))
          .toList();

      if (missingLines.isNotEmpty) {
        final needsLeadingNewline =
            existing.isNotEmpty && !existing.endsWith('\n');
        final prefix = needsLeadingNewline ? '\n' : '';
        await attributesFile.writeAsString(
          '$prefix${missingLines.join('\n')}\n',
          mode: FileMode.append,
        );
        ggLog('Updated .gitattributes in $repoName.');
      }
    }

    // Configure the `ours` merge driver locally so the `merge=ours`
    // rules in .gitattributes resolve to a real git driver.
    final gitDir = Directory(path.join(repoDir.path, '.git'));
    if (!gitDir.existsSync()) {
      ggLog(
        cWarn(
          'Skipping merge.ours driver config for $repoName because no '
          '.git directory was found.',
        ),
      );
      continue;
    }

    final result = await runner(
      'git',
      <String>['config', 'merge.ours.driver', 'true'],
      workingDirectory: repoDir.path,
      runInShell: ggPlatform.isWindows,
    );

    if (result.exitCode != 0) {
      ggLog(
        cError(
          'Failed to configure merge.ours driver in $repoName: '
          '${result.stderr}',
        ),
      );
      throw Exception(
        cError('git config merge.ours.driver true failed in $repoName'),
      );
    }

    ggLog('Configured merge.ours driver in $repoName.');
  }

  ggLog(
    '✓ Ensured .gitattributes for all repositories in ticket '
    '$ticketName.',
  );
}