From 32d5831a4dc3add6a24be82cfa6889bc1cf67501 Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Wed, 19 Mar 2025 09:40:22 +0000 Subject: [PATCH] fix: updating pins with `subdir` which track Alire-generated files (#1906) * Update test * Fix updating pins with `subdir` which track Alire-generated files --- src/alire/alire-user_pins.adb | 14 +- testsuite/drivers/alr.py | 18 +- .../tests/pin/branch-update-dirty/test.py | 210 ++++++++++-------- 3 files changed, 141 insertions(+), 101 deletions(-) diff --git a/src/alire/alire-user_pins.adb b/src/alire/alire-user_pins.adb index 5fab51ee..4953065c 100644 --- a/src/alire/alire-user_pins.adb +++ b/src/alire/alire-user_pins.adb @@ -250,7 +250,9 @@ package body Alire.User_Pins is -- checkout in the 'alire/cache/pins' directory themselves, so -- we require user confirmation before discarding anything. declare - Paths : constant AAA.Strings.Set := + use AAA.Strings; + + Paths : constant Set := VCSs.Git.Handler.Dirty_Files (Destination, Include_Untracked => True); @@ -258,10 +260,16 @@ package body Alire.User_Pins is Paths_List : constant String := List_Sep & Paths.To_Vector.Flatten (List_Sep); + Subdir_Prefix : constant String := + (if This.Subdir /= "" + then (String (Alire.VFS.To_Portable (+This.Subdir)) & "/") + else ""); + Alire_Generated_Dirs : constant Vector := + Empty_Vector & "alire/" & "config/"; Alire_Generated_Dirs_Only : constant Boolean := (for all Path of Paths => - AAA.Strings.Has_Prefix (Path, "alire/") - or else AAA.Strings.Has_Prefix (Path, "config/")); + (for some Generated_Dir of Alire_Generated_Dirs => + Has_Prefix (Path, Subdir_Prefix & Generated_Dir))); -- 'git status' yields '/' separated paths, even on Windows Question : constant String := diff --git a/testsuite/drivers/alr.py b/testsuite/drivers/alr.py index 007faf27..d842abab 100644 --- a/testsuite/drivers/alr.py +++ b/testsuite/drivers/alr.py @@ -412,7 +412,7 @@ def alr_unpin(crate, manual=True, fail_if_missing=True, update=True): run_alr("pin", "--unpin", crate) -def alr_pin(crate, version="", path="", url="", commit="", branch="", +def alr_pin(crate, version="", path="", url="", commit="", branch="", subdir="", manual=True, update=True, force=False): """ Pin a crate, either manually or using the command-line interface. Use only @@ -430,12 +430,15 @@ def alr_pin(crate, version="", path="", url="", commit="", branch="", pin_line = f'{crate} = {{ version = "{version}" }}' elif path != "": pin_line = f"{crate} = {{ path = '{path}' }}" # literal so \ works - elif url != "" and commit != "": - pin_line = f"{crate} = {{ url = '{url}', commit = '{commit}' }}" - elif url != "" and branch != "": - pin_line = f"{crate} = {{ url = '{url}', branch = '{branch}' }}" elif url != "": - pin_line = f"{crate} = {{ url = '{url}' }}" + if branch != "": + rev_part = f", branch = '{branch}'" + elif commit != "": + rev_part = f", commit = '{commit}'" + else: + rev_part = "" + subdir_part = f", subdir = '{subdir}'" if subdir != "" else "" + pin_line = f"{crate} = {{ url = '{url}'{rev_part}{subdir_part} }}" else: raise ValueError("Specify either version, path or url") @@ -469,6 +472,9 @@ def alr_pin(crate, version="", path="", url="", commit="", branch="", elif branch != "": args += ["--branch", f"{branch}"] + if subdir != "": + args += ["--subdir", f"{subdir}"] + return run_alr("pin", *args, force=force) diff --git a/testsuite/tests/pin/branch-update-dirty/test.py b/testsuite/tests/pin/branch-update-dirty/test.py index 407ad456..2f7e5c36 100644 --- a/testsuite/tests/pin/branch-update-dirty/test.py +++ b/testsuite/tests/pin/branch-update-dirty/test.py @@ -5,13 +5,14 @@ manual changes to the cached clone. import os +import pathlib import re import shutil import subprocess from drivers.alr import run_alr, alr_pin, init_local_crate from drivers.asserts import assert_match, assert_in_file, assert_not_substring -from drivers.helpers import git_commit_file, init_git_repo, replace_in_file +from drivers.helpers import git_commit_file, init_git_repo, replace_in_file, git_blast def run(args): @@ -20,99 +21,124 @@ def run(args): return p -# Create crate yyy, with git tracking the 'config/*' files -init_local_crate("yyy") -yyy_path = os.getcwd() -replace_in_file(".gitignore", "/config/\n", "") -init_git_repo(".") -os.chdir("..") - -# Create and build another crate, with yyy's default branch added as a pin -init_local_crate() -xxx_path = os.getcwd() -alr_pin("yyy", url=f"git+file:{yyy_path}") -run_alr("build") - -# Verify that the cached copy of yyy has a dirty repo (due to Alire's changes to -# the 'config/*' files during the build) -cached_yyy_path = os.path.join(xxx_path, "alire", "cache", "pins", "yyy") -gpr_rel_path = os.path.join("config", "yyy_config.gpr") -gpr_rel_path_pattern = r"config/yyy_config\.gpr" # with '/', as returned by Git -os.chdir(cached_yyy_path) -p = run(["git", "status", "--porcelain"]) -assert_match(rf".* M {gpr_rel_path_pattern}", p.stdout.decode()) - -# Add commits to yyy's default branch, including a change to a file in 'config/' -os.chdir(yyy_path) -git_commit_file("Change_config", gpr_rel_path, "This is a new addition\n", "a") -git_commit_file("Add_test_file", "test_file", "This is a new file\n") - -# Check that the dirty repo doesn't prevent updating the pin (subject to user -# confirmation) -os.chdir(xxx_path) -p = run_alr("update") -assert_match( - ( +for subdir in ("", "subdir", os.path.join("subdir", "subsubdir")): + # Create crate yyy, with the 'config/*' files not `.gitignore`ed + init_local_crate("yyy") + replace_in_file(".gitignore", "/config/\n", "") + os.chdir("..") + + # Move this crate to the desired subdirectory of `yyy` + shutil.move("yyy", "temp") + shutil.move("temp", os.path.join("yyy", subdir)) + + # Setup a Git repository with `yyy/` as the root + yyy_path = os.path.realpath("yyy") + init_git_repo(yyy_path) + + # Create and build another crate, with yyy's default branch added as a pin + init_local_crate() + xxx_path = os.getcwd() + alr_pin("yyy", url=f"git+file:{yyy_path}", subdir=subdir) + run_alr("build") + + # Verify that the cached copy of yyy has a dirty repo (due to Alire's + # changes to the 'config/*' files during the build) + cached_yyy_path = os.path.join(xxx_path, "alire", "cache", "pins", "yyy") + rel_config_dir_path = os.path.join(subdir, "config") + gpr_rel_path = os.path.join(rel_config_dir_path, "yyy_config.gpr") + # Git's output uses '/', even on Windows + gpr_rel_path_pattern = re.escape(pathlib.Path(gpr_rel_path).as_posix()) + os.chdir(cached_yyy_path) + p = run(["git", "status", "--porcelain"]) + assert_match(rf".* M {gpr_rel_path_pattern}", p.stdout.decode()) + + # Add commits to yyy's default branch, including a change to a file in + # 'config/' + os.chdir(yyy_path) + git_commit_file( + "Change_config", gpr_rel_path, "This is a new addition\n", "a" + ) + git_commit_file("Add_test_file", "test_file", "This is a new file\n") + + # Check that the dirty repo doesn't prevent updating the pin (subject to + # user confirmation) + os.chdir(xxx_path) + p = run_alr("update") + assert_match( + ( + ".*Updating the pin 'yyy' will discard local uncommitted changes " + f"in '{re.escape(cached_yyy_path)}' to the following:" + rf".*[\r\n]+ {gpr_rel_path_pattern}" + r".*[\r\n]+These changes affect only Alire's automatically " + r"generated files, which are safe to overwrite\.[\r\n]+Do you want " + r"to proceed\?" + ), + p.out + ) + + # Check that the update was successful + assert_in_file( + os.path.join(cached_yyy_path, "test_file"), "This is a new file" + ) + + # Reset the cached clone by only one commit and repeat the above. This time + # the update should work without user confirmation, as there will be no + # conflict (only 'test_file' needs to be updated, which is not dirty). + os.chdir(cached_yyy_path) + run(["git", "reset", "--hard", "HEAD~"]) + shutil.rmtree(rel_config_dir_path) # So 'alr build' re-generates the files. + os.chdir(xxx_path) + run_alr("build") + os.chdir(cached_yyy_path) + p = run(["git", "status", "--porcelain"]) + assert_match(rf".* M {gpr_rel_path_pattern}", p.stdout.decode()) + os.chdir(xxx_path) + p = run_alr("update") + assert_not_substring("will discard local uncommitted changes", p.out) + assert_in_file( + os.path.join(cached_yyy_path, "test_file"), "This is a new file" + ) + + # Reset by one commit and repeat the process again, but write to 'test_file' + # so that it is dirty and there is a conflict. Since this time the change is + # not in one of Alire's auto-generated subdirectories, the prompt should be + # different. + # + # This also checks that conflicts involving untracked files are treated the + # same as those with tracked files. + os.chdir(cached_yyy_path) + run(["git", "reset", "--hard", "HEAD~"]) + shutil.rmtree(rel_config_dir_path) + os.chdir(xxx_path) + run_alr("build") + os.chdir(cached_yyy_path) + with open("test_file", "w") as f: + f.write("This file is now dirty.\n") + p = run(["git", "status", "--porcelain"]) + assert_match(rf".* M {gpr_rel_path_pattern}", p.stdout.decode()) + assert_match(r".*\?\? test_file", p.stdout.decode()) + os.chdir(xxx_path) + prompt = ( ".*Updating the pin 'yyy' will discard local uncommitted changes in " f"'{re.escape(cached_yyy_path)}' to the following:" - rf".*[\r\n]+ {gpr_rel_path_pattern}" - r".*[\r\n]+These changes affect only Alire's automatically generated " - r"files, which are safe to overwrite\.[\r\n]+Do you want to proceed\?" - ), - p.out -) - -# Check that the update was successful -assert_in_file(os.path.join(cached_yyy_path, "test_file"), "This is a new file") - -# Reset the cached clone by only one commit and repeat the above. This time the -# update should work without user confirmation, as there will be no conflict -# (only 'test_file' needs to be updated, which is not dirty). -os.chdir(cached_yyy_path) -run(["git", "reset", "--hard", "HEAD~"]) -shutil.rmtree("config") # So 'alr build' auto-generates the same files as above. -os.chdir(xxx_path) -run_alr("build") -os.chdir(cached_yyy_path) -p = run(["git", "status", "--porcelain"]) -assert_match(rf".* M {gpr_rel_path_pattern}", p.stdout.decode()) -os.chdir(xxx_path) -p = run_alr("update") -assert_not_substring("will discard local uncommitted changes", p.out) -assert_in_file(os.path.join(cached_yyy_path, "test_file"), "This is a new file") - -# Reset by one commit and repeat the process again, but write to 'test_file' so -# that it is dirty and there is a conflict. Since this time the change is not in -# one of Alire's auto-generated subdirectories, the prompt should be different. -# -# This also checks that conflicts involving untracked files are treated the same -# as those with tracked files. -os.chdir(cached_yyy_path) -run(["git", "reset", "--hard", "HEAD~"]) -shutil.rmtree("config") -os.chdir(xxx_path) -run_alr("build") -os.chdir(cached_yyy_path) -with open("test_file", "w") as f: - f.write("This file is now dirty.\n") -p = run(["git", "status", "--porcelain"]) -assert_match(rf".* M {gpr_rel_path_pattern}", p.stdout.decode()) -assert_match(r".*\?\? test_file", p.stdout.decode()) -os.chdir(xxx_path) -prompt = ( - ".*Updating the pin 'yyy' will discard local uncommitted changes in " - f"'{re.escape(cached_yyy_path)}' to the following:" - rf".*[\r\n]+ {gpr_rel_path_pattern}.*[\r\n]+ test_file" - r".*[\r\n]+These changes include files which were not automatically " - r"generated by Alire\.[\r\n]+Are you sure you want to proceed\?[\r\n]+" -) -# The default response should be 'No', so 'alr -n update' will fail. -p = run_alr("update", complain_on_error=False) -assert_match(prompt + "Using default: No", p.out) -# Supplying '--force' should change the default to 'Yes'. -p = run_alr("-f", "update") -assert_match(prompt + "Using default: Yes", p.out) -assert_in_file(os.path.join(cached_yyy_path, "test_file"), "This is a new file") + rf".*[\r\n]+ {gpr_rel_path_pattern}.*[\r\n]+ test_file" + r".*[\r\n]+These changes include files which were not automatically " + r"generated by Alire\.[\r\n]+Are you sure you want to proceed\?[\r\n]+" + ) + # The default response should be 'No', so 'alr -n update' will fail. + p = run_alr("update", complain_on_error=False) + assert_match(prompt + "Using default: No", p.out) + # Supplying '--force' should change the default to 'Yes'. + p = run_alr("-f", "update") + assert_match(prompt + "Using default: Yes", p.out) + assert_in_file( + os.path.join(cached_yyy_path, "test_file"), "This is a new file" + ) + + # Clean up for the next iteration + os.chdir("..") + git_blast("xxx") + git_blast("yyy") print('SUCCESS') -- 2.39.5