diff --git a/first_time_install.sh b/first_time_install.sh old mode 100644 new mode 100755 diff --git a/scripts/install/install_service.sh b/scripts/install/install_service.sh old mode 100644 new mode 100755 diff --git a/scripts/install/install_web_service.sh b/scripts/install/install_web_service.sh old mode 100644 new mode 100755 diff --git a/start_display.sh b/start_display.sh old mode 100644 new mode 100755 diff --git a/stop_display.sh b/stop_display.sh old mode 100644 new mode 100755 diff --git a/test/test_git_pull_resolution.py b/test/test_git_pull_resolution.py index d5668142..4d235df5 100644 --- a/test/test_git_pull_resolution.py +++ b/test/test_git_pull_resolution.py @@ -58,7 +58,7 @@ def repos(tmp_path): def test_branch_with_upstream_uses_a_plain_pull(repos): args, note, error = resolve_pull_command(str(repos)) assert error is None - assert args == ['git', 'pull', '--rebase'] + assert args == ['git', 'pull', '--rebase', '--autostash'] assert note == '' @@ -73,7 +73,7 @@ def test_branch_without_upstream_falls_back_to_origin_branch(repos): args, note, error = resolve_pull_command(str(repos)) assert error is None - assert args == ['git', 'pull', '--rebase', 'origin', 'audit'] + assert args == ['git', 'pull', '--rebase', '--autostash', 'origin', 'audit'] assert 'audit' in note @@ -155,7 +155,7 @@ def test_switching_attaches_tracking_so_pull_needs_no_fallback(repos): args, note, error = resolve_pull_command(str(repos)) assert error is None - assert args == ['git', 'pull', '--rebase'] + assert args == ['git', 'pull', '--rebase', '--autostash'] assert note == '' @@ -200,3 +200,37 @@ def test_stash_option_lets_the_switch_through_and_keeps_the_work(repos): assert _git('branch', '--show-current', cwd=repos).stdout.strip() == 'other' # The edit is not lost — it is on the stash. assert 'switch to other' in _git('stash', 'list', cwd=repos).stdout + + +class TestInstallerDoesNotBlockTheUpdateButton: + """first_time_install.sh chmods scripts that git tracked as 644. + + With core.fileMode true -- the default on Linux -- that leaves five + permanently modified tracked files on every machine that ran the + installer, and `git pull --rebase` refuses to start: + + error: cannot pull with rebase: You have unstaged changes. + + Tracking them as executable makes the installer's chmod a no-op. + """ + + CHMODDED = [ + 'first_time_install.sh', + 'start_display.sh', + 'stop_display.sh', + 'scripts/install/install_service.sh', + 'scripts/install/install_web_service.sh', + ] + + def test_scripts_the_installer_chmods_are_tracked_executable(self): + import subprocess + from pathlib import Path + root = Path(__file__).resolve().parent.parent + out = subprocess.run(['git', 'ls-files', '-s', *self.CHMODDED], + capture_output=True, text=True, cwd=str(root)).stdout + modes = {line.split()[3]: line.split()[0] for line in out.strip().split('\n') if line} + non_exec = sorted(f for f, m in modes.items() if m != '100755') + assert not non_exec, ( + f"{non_exec} are chmodded by the installer but tracked non-executable, " + "so every install leaves the working tree dirty and the update " + "button cannot pull") diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 984637e8..c1d2c03b 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -1657,13 +1657,22 @@ def resolve_pull_command(project_dir): backup, or following an install guide that names one. The update button then reports a failure the user cannot act on. + ``--autostash`` is passed for the same reason. Rebase refuses to start + when any tracked file is modified, and on these installs something always + is: first_time_install.sh chmods five scripts that git tracked as 644, so + every machine that ran the installer carries five permanent mode changes + and the update button reports "cannot pull with rebase: You have unstaged + changes". Those modes are corrected in this commit, but a user cannot pull + the correction while the pull is what is blocked, and any other local edit + would reproduce it anyway. Autostash reapplies the changes afterwards. + Returns ``(args, note, error)``. When ``origin/`` exists the pull is made explicit against it, so the update proceeds and the branch is given tracking information afterwards. """ upstream = _git_upstream(project_dir) if upstream: - return ['git', 'pull', '--rebase'], '', None + return ['git', 'pull', '--rebase', '--autostash'], '', None branch = _git_current_branch(project_dir) if not branch: @@ -1673,7 +1682,7 @@ def resolve_pull_command(project_dir): ) if _git_remote_branch_exists(project_dir, branch): return ( - ['git', 'pull', '--rebase', 'origin', branch], + ['git', 'pull', '--rebase', '--autostash', 'origin', branch], f"Branch '{branch}' had no upstream; pulled from origin/{branch} and set it as the upstream.", None, )