|
@@ -0,0 +1,392 @@
|
|
|
|
|
+using Invercargill.DataStructures;
|
|
|
|
|
+using InvercargillJson;
|
|
|
|
|
+
|
|
|
|
|
+/** Default managed state root, matching the layout `usm deploy` writes into images. */
|
|
|
|
|
+private const string ENROLL_DEFAULT_STATE_PATH = "/var/usm";
|
|
|
|
|
+
|
|
|
|
|
+/** Directory mode for every directory enrollment creates. */
|
|
|
|
|
+private const int ENROLL_DIRECTORY_MODE = 0755;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * `usm enroll [-y|--yes] [--state-path <path>]` — transition a system from
|
|
|
|
|
+ * unmanaged to managed by USM.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Shows the exact directories that will be created and the usm.config
|
|
|
|
|
+ * change, asks for confirmation (skipped with `-y`/`--yes`), sets
|
|
|
|
|
+ * `is_managed: true` plus `managed.state_path` in usm.config (creating or
|
|
|
|
|
+ * updating the file, honouring USM_CONFIGDIR) and creates every directory
|
|
|
|
|
+ * USM references at runtime: the managed state tree (`packages/`,
|
|
|
|
|
+ * `installed/`, `lists/` — see {@link Usm.SystemState}), the configuration
|
|
|
|
|
+ * tree (`usm.config`, `repos.d/`) and the destination/prefix directory
|
|
|
|
|
+ * layout behind {@link Usm.Paths.get_suggested_base_path_for_type} and
|
|
|
|
|
+ * {@link Usm.Paths.set_envs}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Idempotent: existing directories are left alone, and a system whose
|
|
|
|
|
+ * config is already managed with the same state path reports "already
|
|
|
|
|
+ * enrolled" after re-ensuring the directories exist. Without root the
|
|
|
|
|
+ * default state path and the system directory tree usually cannot be
|
|
|
|
|
+ * written; enrollment warns and suggests a user-local
|
|
|
|
|
+ * `--state-path ~/.local/state/usm` instead.
|
|
|
|
|
+ */
|
|
|
|
|
+public int enroll_main(string[] args) throws Error {
|
|
|
|
|
+ var assume_yes = false;
|
|
|
|
|
+ string? state_path_flag = null;
|
|
|
|
|
+ for(int i = 2; i < args.length; i++) {
|
|
|
|
|
+ var argument = args[i];
|
|
|
|
|
+ if(argument == "-y" || argument == "--yes") {
|
|
|
|
|
+ assume_yes = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ else if(argument.has_prefix("--state-path=")) {
|
|
|
|
|
+ state_path_flag = argument.split("=", 2)[1];
|
|
|
|
|
+ }
|
|
|
|
|
+ else if(argument == "--state-path") {
|
|
|
|
|
+ if(i + 1 >= args.length) {
|
|
|
|
|
+ printerr("Expected a path after --state-path\n");
|
|
|
|
|
+ return enroll_usage();
|
|
|
|
|
+ }
|
|
|
|
|
+ state_path_flag = args[++i];
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ printerr(@"Unexpected argument \"$argument\"\n");
|
|
|
|
|
+ return enroll_usage();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if(state_path_flag != null && !Path.is_absolute(state_path_flag)) {
|
|
|
|
|
+ state_path_flag = Path.build_filename(Environment.get_current_dir(), state_path_flag);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var config_file = Path.build_filename(paths.usm_config_dir, "usm.config");
|
|
|
|
|
+ Usm.Configuration? existing_config = null;
|
|
|
|
|
+ if(File.new_for_path(config_file).query_exists()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ existing_config = new Usm.Configuration.from_paths(paths);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ printerr(@"The existing usm.config could not be read: $(e.message)\n");
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // An explicit --state-path re-enrols to a new root; otherwise an
|
|
|
|
|
+ // already-managed config keeps its own state path
|
|
|
|
|
+ var state_path = state_path_flag
|
|
|
|
|
+ ?? existing_config?.managed_config?.state_path
|
|
|
|
|
+ ?? ENROLL_DEFAULT_STATE_PATH;
|
|
|
|
|
+
|
|
|
|
|
+ if(existing_config != null && existing_config.is_managed
|
|
|
|
|
+ && existing_config.managed_config != null
|
|
|
|
|
+ && existing_config.managed_config.state_path == state_path) {
|
|
|
|
|
+ print(@"This system is already enrolled; managed state lives in \"$state_path\".\n");
|
|
|
|
|
+ var plan = enroll_plan_directories(state_path);
|
|
|
|
|
+ var outcome = enroll_ensure_directories(plan);
|
|
|
|
|
+ enroll_report(outcome, null);
|
|
|
|
|
+ return enroll_report_failures(plan, outcome);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Rendered before the mutation below: updated_config aliases
|
|
|
|
|
+ // existing_config, so the "old" side of the diff must be captured first
|
|
|
|
|
+ string? existing_config_text = null;
|
|
|
|
|
+ if(existing_config != null) {
|
|
|
|
|
+ existing_config_text = enroll_render_config(existing_config);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var updated_config = existing_config ?? new Usm.Configuration();
|
|
|
|
|
+ updated_config.is_managed = true;
|
|
|
|
|
+ if(updated_config.managed_config == null) {
|
|
|
|
|
+ updated_config.managed_config = new Usm.ManagedConfiguration();
|
|
|
|
|
+ }
|
|
|
|
|
+ updated_config.managed_config.state_path = state_path;
|
|
|
|
|
+
|
|
|
|
|
+ var plan = enroll_plan_directories(state_path);
|
|
|
|
|
+ print("Enrolling this system with USM will:\n\n");
|
|
|
|
|
+ print("Create these directories (existing ones are left alone):\n");
|
|
|
|
|
+ enroll_print_directories("State", plan.state_directories);
|
|
|
|
|
+ enroll_print_directories("Configuration", plan.config_directories);
|
|
|
|
|
+ enroll_print_directories("System", plan.system_directories);
|
|
|
|
|
+
|
|
|
|
|
+ print("\nChange the configuration:\n");
|
|
|
|
|
+ if(existing_config == null) {
|
|
|
|
|
+ print(@" create $config_file:\n");
|
|
|
|
|
+ foreach(var line in enroll_render_config(updated_config).split("\n")) {
|
|
|
|
|
+ print(@" + $line\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ print(@" $config_file:\n");
|
|
|
|
|
+ foreach(var line in enroll_diff_lines(existing_config_text, enroll_render_config(updated_config))) {
|
|
|
|
|
+ print(@" $line\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(Posix.geteuid() != 0) {
|
|
|
|
|
+ if(state_path == ENROLL_DEFAULT_STATE_PATH) {
|
|
|
|
|
+ printerr(@"\nWarning: not running as root — the default state path \"$state_path\" and the system directories usually require root.\n");
|
|
|
|
|
+ printerr("Re-run under sudo, or enrol user-local instead: usm enroll --state-path ~/.local/state/usm\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ printerr("\nWarning: not running as root — the system directories above usually require root and may fail to create.\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(!assume_yes && !enroll_confirm()) {
|
|
|
|
|
+ print("Aborted.\n");
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var result = enroll_ensure_directories(plan);
|
|
|
|
|
+ // A config claiming a state path that could not be created would leave
|
|
|
|
|
+ // the system half-managed: fail before writing it
|
|
|
|
|
+ var essential_failed = result.failed.any(d => plan.state_directories.contains(d) || plan.config_directories.contains(d));
|
|
|
|
|
+ if(essential_failed) {
|
|
|
|
|
+ enroll_report(result, null);
|
|
|
|
|
+ return enroll_report_failures(plan, result);
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ new JsonElement.from_properties(Usm.Configuration.get_mapper().map_from(updated_config)).write_to_file(config_file);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ printerr(@"Unable to write \"$config_file\": $(e.message)\n");
|
|
|
|
|
+ enroll_report(result, null);
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ enroll_report(result, existing_config == null
|
|
|
|
|
+ ? @"Created configuration $config_file"
|
|
|
|
|
+ : @"Updated configuration $config_file");
|
|
|
|
|
+ return enroll_report_failures(plan, result);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private int enroll_usage() {
|
|
|
|
|
+ printerr("USAGE:\n\tusm enroll [-y|--yes] [--state-path <path>]\n");
|
|
|
|
|
+ return 255;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * The complete directory plan: managed state tree, configuration tree and
|
|
|
|
|
+ * the destination/prefix layout, in creation order.
|
|
|
|
|
+ */
|
|
|
|
|
+private class EnrollPlan {
|
|
|
|
|
+ public Vector<string> state_directories = new Vector<string>();
|
|
|
|
|
+ public Vector<string> config_directories = new Vector<string>();
|
|
|
|
|
+ public Vector<string> system_directories = new Vector<string>();
|
|
|
|
|
+
|
|
|
|
|
+ public Vector<string> all() {
|
|
|
|
|
+ var every = new Vector<string>();
|
|
|
|
|
+ every.add_all(state_directories);
|
|
|
|
|
+ every.add_all(config_directories);
|
|
|
|
|
+ every.add_all(system_directories);
|
|
|
|
|
+ return every;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Every directory USM references at runtime for the given state path: the
|
|
|
|
|
+ * {@link Usm.SystemState} layout (`packages`, `installed`, `lists`),
|
|
|
|
|
+ * `usm.config`'s directory plus `repos.d`, and one directory per
|
|
|
|
|
+ * {@link Usm.Paths.get_suggested_base_path_for_type} branch plus the
|
|
|
|
|
+ * LOCALSTATEDIR/SHAREDSTATEDIR roots {@link Usm.Paths.set_envs} exports.
|
|
|
|
|
+ * Equal paths (lib == canonlib, for instance) collapse to one entry.
|
|
|
|
|
+ */
|
|
|
|
|
+private EnrollPlan enroll_plan_directories(string state_path) {
|
|
|
|
|
+ var plan = new EnrollPlan();
|
|
|
|
|
+
|
|
|
|
|
+ plan.state_directories.add(state_path);
|
|
|
|
|
+ foreach(var part in new string[] { "packages", "installed", "lists" }) {
|
|
|
|
|
+ plan.state_directories.add(Path.build_filename(state_path, part));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ plan.config_directories.add(paths.usm_config_dir);
|
|
|
|
|
+ plan.config_directories.add(Path.build_filename(paths.usm_config_dir, "repos.d"));
|
|
|
|
|
+
|
|
|
|
|
+ var system = plan.system_directories;
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.ROOT_PATH));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.PATH));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.OPTIONAL));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.RESOURCE));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.CONFIGURATION));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.BINARY));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.SUPER_BINARY));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.LIBRARY));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.LIBRARY_RESOURCE));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.LIBRARY_EXECUTABLE));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.CANONICAL_LIBRARY));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.CANONICAL_LIBRARY_RESOURCE));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.GIO_MODULE));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.INFO_PAGE));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.MANUAL_PAGE));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.LOCALE));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.APPLICATION));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.INCLUDE));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.PKG_CONFIG));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.VALA_API));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.GOBJECT_IR));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.TYPELIB));
|
|
|
|
|
+ enroll_add_unique(system, paths.get_suggested_base_path_for_type(Usm.ResourceType.TAG));
|
|
|
|
|
+ enroll_add_unique(system, Path.build_filename(paths.destination, paths.local_state));
|
|
|
|
|
+ enroll_add_unique(system, Path.build_filename(paths.destination, paths.shared_state));
|
|
|
|
|
+ return plan;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private void enroll_add_unique(Vector<string> directories, string directory) {
|
|
|
|
|
+ if(!directories.contains(directory)) {
|
|
|
|
|
+ directories.add(directory);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** What a directory-creation pass did: created, skipped and failed paths. */
|
|
|
|
|
+private class EnrollOutcome {
|
|
|
|
|
+ public Vector<string> created = new Vector<string>();
|
|
|
|
|
+ public Vector<string> existed = new Vector<string>();
|
|
|
|
|
+ public Vector<string> failed = new Vector<string>();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private EnrollOutcome enroll_ensure_directories(EnrollPlan plan) {
|
|
|
|
|
+ var outcome = new EnrollOutcome();
|
|
|
|
|
+ foreach(var directory in plan.all()) {
|
|
|
|
|
+ if(File.new_for_path(directory).query_exists()) {
|
|
|
|
|
+ outcome.existed.add(directory);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ DirUtils.create_with_parents(directory, ENROLL_DIRECTORY_MODE);
|
|
|
|
|
+ outcome.created.add(directory);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ outcome.failed.add(directory);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return outcome;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Exit-code half of the outcome report: failures in the state and
|
|
|
|
|
+ * configuration trees are fatal (enrollment is not functional without
|
|
|
|
|
+ * them), while system-tree failures — typically needing root on a live
|
|
|
|
|
+ * system — only warn.
|
|
|
|
|
+ */
|
|
|
|
|
+private int enroll_report_failures(EnrollPlan plan, EnrollOutcome outcome) {
|
|
|
|
|
+ if(outcome.failed.count() == 0) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ printerr("\nFailed to create:\n");
|
|
|
|
|
+ foreach(var directory in outcome.failed) {
|
|
|
|
|
+ printerr(@" $directory\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ var essential = outcome.failed.any(d => plan.state_directories.contains(d) || plan.config_directories.contains(d));
|
|
|
|
|
+ if(essential) {
|
|
|
|
|
+ printerr("The state and configuration directories are required; re-run as root or pass a writable --state-path.\n");
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ printerr("These system directories need root; the rest of the enrollment is in place.\n");
|
|
|
|
|
+ return 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private void enroll_report(EnrollOutcome outcome, string? config_action) {
|
|
|
|
|
+ foreach(var directory in outcome.created) {
|
|
|
|
|
+ print("%-9s %s\n", "Created", directory);
|
|
|
|
|
+ }
|
|
|
|
|
+ if(outcome.existed.count() > 0) {
|
|
|
|
|
+ print(@"$(outcome.existed.count()) directories already existed (left alone)\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ if(config_action != null) {
|
|
|
|
|
+ print(@"$config_action\n");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private void enroll_print_directories(string title, Vector<string> directories) {
|
|
|
|
|
+ print(@"$title:\n");
|
|
|
|
|
+ foreach(var directory in directories) {
|
|
|
|
|
+ var suffix = File.new_for_path(directory).query_exists() ? " (already exists)" : "";
|
|
|
|
|
+ print(@" $directory$suffix\n");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** The yes/no confirmation; anything but an explicit yes (or EOF) declines. */
|
|
|
|
|
+private bool enroll_confirm() {
|
|
|
|
|
+ print("Enrol this system? [y/N] ");
|
|
|
|
|
+ stdout.flush();
|
|
|
|
|
+ var reply = enroll_read_line();
|
|
|
|
|
+ if(reply == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ var answer = reply.strip().down();
|
|
|
|
|
+ return answer == "y" || answer == "yes";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** One line from stdin, or null at EOF; FileStream exposes no read_line. */
|
|
|
|
|
+private string? enroll_read_line() {
|
|
|
|
|
+ var builder = new StringBuilder();
|
|
|
|
|
+ var character = stdin.getc();
|
|
|
|
|
+ while(character != -1 && character != '\n') {
|
|
|
|
|
+ builder.append_c((char)character);
|
|
|
|
|
+ character = stdin.getc();
|
|
|
|
|
+ }
|
|
|
|
|
+ if(character == -1 && builder.len == 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return builder.str;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** A {@link Usm.Configuration} rendered exactly as it is written to usm.config. */
|
|
|
|
|
+private string enroll_render_config(Usm.Configuration config) throws Error {
|
|
|
|
|
+ var properties = Usm.Configuration.get_mapper().map_from(config);
|
|
|
|
|
+ return new JsonElement.from_properties(properties).stringify_pretty();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Unified-style line diff (" " shared, "-" old-only, "+" new-only) via
|
|
|
|
|
+ * longest common subsequence — both sides are mapper-rendered, so the
|
|
|
|
|
+ * diff shows semantic changes rather than formatting churn.
|
|
|
|
|
+ */
|
|
|
|
|
+private string[] enroll_diff_lines(string old_text, string new_text) {
|
|
|
|
|
+ var old_lines = enroll_split_lines(old_text);
|
|
|
|
|
+ var new_lines = enroll_split_lines(new_text);
|
|
|
|
|
+ var old_count = old_lines.length;
|
|
|
|
|
+ var new_count = new_lines.length;
|
|
|
|
|
+
|
|
|
|
|
+ var table = new int[old_count + 1, new_count + 1];
|
|
|
|
|
+ for(var i = old_count - 1; i >= 0; i--) {
|
|
|
|
|
+ for(var j = new_count - 1; j >= 0; j--) {
|
|
|
|
|
+ table[i, j] = old_lines[i] == new_lines[j]
|
|
|
|
|
+ ? table[i + 1, j + 1] + 1
|
|
|
|
|
+ : int.max(table[i + 1, j], table[i, j + 1]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var diff = new Vector<string>();
|
|
|
|
|
+ var a = 0;
|
|
|
|
|
+ var b = 0;
|
|
|
|
|
+ while(a < old_count && b < new_count) {
|
|
|
|
|
+ if(old_lines[a] == new_lines[b]) {
|
|
|
|
|
+ diff.add(@" $(old_lines[a])");
|
|
|
|
|
+ a++;
|
|
|
|
|
+ b++;
|
|
|
|
|
+ }
|
|
|
|
|
+ else if(table[a + 1, b] >= table[a, b + 1]) {
|
|
|
|
|
+ diff.add(@"- $(old_lines[a])");
|
|
|
|
|
+ a++;
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ diff.add(@"+ $(new_lines[b])");
|
|
|
|
|
+ b++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ while(a < old_count) {
|
|
|
|
|
+ diff.add(@"- $(old_lines[a++])");
|
|
|
|
|
+ }
|
|
|
|
|
+ while(b < new_count) {
|
|
|
|
|
+ diff.add(@"+ $(new_lines[b++])");
|
|
|
|
|
+ }
|
|
|
|
|
+ return diff.to_array();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+private string[] enroll_split_lines(string text) {
|
|
|
|
|
+ var lines = text.split("\n", -1);
|
|
|
|
|
+ if(lines.length > 0 && lines[lines.length - 1] == "") {
|
|
|
|
|
+ lines.resize(lines.length - 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return lines;
|
|
|
|
|
+}
|