Keywords: Git permissions | directory write permissions | unlink operation
Abstract: This paper provides an in-depth analysis of the 'Unable to unlink old (Permission denied)' error during Git push operations, revealing that the root cause lies in directory write permissions rather than file permissions. Through detailed permission mechanism analysis, code examples, and practical scenario validation, it offers comprehensive solutions and best practices to help developers completely resolve such permission issues.
Problem Phenomenon and Background
In remote server environments, when post-receive hooks are configured to automatically perform Git checkout operations, developers frequently encounter a confusing permission error. The specific manifestation is:
#!/bin/sh
GIT_WORK_TREE=/var/www/<website> git checkout -f
When pushing from a local machine to the server Git repository, the system repeatedly reports:
remote: error: unable to unlink old '<file>' (Permission denied)
This error occurs repeatedly for almost all files, even when these files have exactly the same ownership and permissions as README.txt files that operate normally:
-rw-r--r-- 1 <serverusername> <serverusername> 2939 Aug 2 10:58 README.txt
Root Cause Analysis
The core of the problem lies in misunderstanding the Unix/Linux file system permission mechanism. When Git needs to update or delete files, it actually performs operations on the directory containing the file, not directly on the file itself.
In Unix file systems, the unlink operation essentially removes the file reference from directory entries. This process requires the following permissions:
- Write permission (w) for the directory containing the file
- Execute permission (x) for the file itself
- Search permission for the directory
Let's understand this mechanism through a concrete code example. Suppose we have a directory structure:
# Check directory permissions
ls -ld /var/www/website
drwxr-xr-x 5 serverusername serverusername 4096 Aug 10 10:00 /var/www/website
# Check file permissions
ls -l /var/www/website/index.html
-rw-r--r-- 1 serverusername serverusername 1024 Aug 10 10:00 index.html
In this scenario, although the files have appropriate read-write permissions, the directory lacks write permission (group and others have no write permission in drwxr-xr-x), which causes the unlink operation to fail.
Solution Implementation
Based on understanding the root cause of the problem, we can provide several effective solutions:
Solution 1: Fix Directory Permissions
The most direct solution is to ensure the Git work tree directory has appropriate write permissions:
# Add write permissions to the directory
sudo chmod -R ug+w /var/www/website
# Verify permission settings
ls -ld /var/www/website
drwxrwxr-x 5 serverusername serverusername 4096 Aug 10 10:00 /var/www/website
This command recursively adds write permissions for owner and group, ensuring Git can perform unlink operations in the directory.
Solution 2: Best Practices for Ownership and Permissions
To build more robust deployment workflows, we recommend adopting the following best practices:
# Create dedicated deployment user and group
sudo groupadd deployers
sudo useradd -g deployers deployuser
# Set directory ownership
sudo chown -R deployuser:deployers /var/www/website
# Set appropriate permissions
sudo chmod -R 775 /var/www/website
sudo find /var/www/website -type f -exec chmod 664 {} \;
Practical Scenario Validation
Referring to similar issues in GitHub Actions, we can see the prevalence of permission problems in automated deployment workflows. In CI/CD environments, ensuring the running user has appropriate permissions for the working directory is crucial:
# Permission fix example in GitHub Actions
- name: Fix permissions
run: |
sudo chown -R $USER:$USER .
sudo chmod -R ug+w .
find . -type f -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;
In-depth Technical Principles
To fully understand this issue, we need to delve into the Unix file system's inode and directory entry mechanisms. Each file in the file system has an inode containing file metadata, while a directory is actually a special file containing a mapping list from filenames to inodes.
When performing an unlink operation:
# Pseudo-code simulating unlink operation
function unlink_file(directory_path, filename) {
# 1. Open directory file (requires directory execute permission)
dir_fd = open(directory_path, O_RDONLY | O_DIRECTORY)
# 2. Remove filename to inode mapping from directory entries (requires directory write permission)
unlinkat(dir_fd, filename, 0)
# 3. If this is the last hard link to the file, release inode and disk space
if (inode.link_count == 0) {
free_inode(inode)
}
}
This process clearly demonstrates why directory permissions are so critical—the core of the unlink operation is modifying the directory file.
Preventive Measures and Best Practices
To avoid similar problems, we recommend implementing the following measures in project deployment workflows:
- Explicitly set directory permissions in deployment scripts
- Use dedicated deployment users and groups
- Regularly audit file system permissions
- Integrate permission checks in CI/CD pipelines
- Use containerized deployment to isolate permission issues
By deeply understanding file system permission mechanisms and Git operation principles, developers can effectively diagnose and resolve such permission issues, ensuring the stability and reliability of deployment workflows.