Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F4031600
D25772.1746701035.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
12 KB
Referenced Files
None
Subscribers
None
D25772.1746701035.diff
View Options
diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
--- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
+++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
@@ -1273,6 +1273,87 @@
}
}
+ public function testProjectDestroy() {
+ $author = $this->generateNewTestUser();
+ $mario = $this->generateNewTestUser();
+ $user_forever_alone = $this->generateNewTestUser();
+
+ // Create a root project that will have children.
+ $a = $this->createProject($author)->save();
+
+ // Add a member directly in the root project, successfully.
+ $this->joinProject($a, $mario);
+
+ // Current project tree:
+ // Project A
+ // \- Member Mario
+ $this->assertEqual(0, $a->getProjectDepth());
+
+ // Create a child project.
+ // Note that the members of "A" are automatically moved to "B".
+ $b = $this->createProject($author, $a)->save();
+
+ // Current project tree:
+ // Project A > Project B
+ // \- Member Mario
+ $this->assertEqual(0, (int)$a->getProjectDepth());
+ $this->assertEqual(1, (int)$b->getProjectDepth());
+
+ // Create a milestone under "B".
+ $m = $this->createProject($author, $b, true)->save();
+
+ // Current project tree:
+ // Project A > Project B > Milestone M
+ $this->assertEqual(0, (int)$a->getProjectDepth());
+ $this->assertEqual(1, (int)$b->getProjectDepth());
+ $this->assertEqual(2, (int)$m->getProjectDepth());
+
+ // Test the addition of another user directly in "A". It must fail.
+ $error_joining_a = false;
+ try {
+ $this->joinProject($a, $user_forever_alone);
+ } catch (PhabricatorApplicationTransactionValidationException $e) {
+ $error_joining_a = true;
+ }
+ $this->assertTrue($error_joining_a);
+
+ // Create a child-child project.
+ // Note that the members of "B" are automatically moved to "C".
+ $c = $this->createProject($author, $b)->save();
+
+ // Current project tree:
+ // Project A > Project B > Milestone M
+ // Project A > Project B > Project C
+ // \- Member Mario
+ $this->assertEqual(0, (int)$a->getProjectDepth());
+ $this->assertEqual(1, (int)$b->getProjectDepth());
+ $this->assertEqual(2, (int)$m->getProjectDepth());
+ $this->assertEqual(2, (int)$c->getProjectDepth());
+
+ // Test the destroy of the project in the middle (B).
+ $engine = new PhabricatorDestructionEngine();
+ $engine->destroyObject($b);
+ $this->refreshProjectInPlace($a, $author);
+ $this->refreshProjectInPlace($c, $author);
+
+ // Current project tree:
+ // Project A > Project C
+ // \- Member Mario
+ $this->assertEqual(0, (int)$a->getProjectDepth());
+ $this->assertEqual(1, (int)$c->getProjectDepth());
+ $this->assertEqual(null, $this->refreshProject($m, $author));
+
+ $engine->destroyObject($a);
+ $this->refreshProjectInPlace($c, $author);
+
+ // Current project tree:
+ // Project C
+ // \- Member Mario
+ $this->assertEqual(0, (int)$c->getProjectDepth());
+
+ // Test the destroy of the last leaf, no crashes.
+ $engine->destroyObject($b);
+ }
private function moveToColumn(
PhabricatorUser $viewer,
@@ -1516,6 +1597,12 @@
}
}
+ private function refreshProjectInPlace(
+ PhabricatorProject &$project,
+ PhabricatorUser $viewer): void {
+ $project = $this->refreshProject($project, $viewer);
+ }
+
private function refreshColumn(
PhabricatorUser $viewer,
PhabricatorProjectColumn $column) {
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -748,9 +748,73 @@
$slug->delete();
}
+ // Destroy my milestones because they cannot live without me.
+ // Do not use PhabricatorProjectQuery to avoid a circular dependency,
+ // and to avoid a silent fail, since these milestones do not
+ // have their parent anymore.
+ $milestones = id(new self())
+ ->loadAllWhere('parentProjectPHID = %s AND milestoneNumber IS NOT NULL',
+ $this->getPHID());
+ foreach ($milestones as $milestone) {
+ $milestone->attachParentProject($this);
+ $engine->destroyObject($milestone);
+ }
+
+ // Update my children to eventually fix gaps in the tree.
+ $this->onDestroyTouchChildren(true);
+
+ // After the tree is fixed, update my parent's hasSubProjects field.
+ if ($this->getParentProject()) {
+ id(new PhabricatorProjectsMembershipIndexEngineExtension())
+ ->rematerialize($this->getParentProject());
+ }
+
$this->saveTransaction();
}
+ /**
+ * On destroy, eventually bubble up my direct children, to take my place.
+ * Refresh all remaining children, to consolidate depth, path key, etc.
+ */
+ private function onDestroyTouchChildren($close_my_hole) {
+ // Micro-optimization.
+ if (!$this->supportsSubprojects() && !$this->supportsMilestones()) {
+ return;
+ }
+
+ // Get direct sub-projects and milestones and their new desired parent.
+ // Do not use PhabricatorProjectQuery to avoid a circular dependency.
+ $query_children = new self();
+ if ($close_my_hole) {
+ // We must skip my direct milestones since they are under removal.
+ $desired_parent = $this->getParentProject();
+ $children = $query_children->loadAllWhere(
+ 'parentProjectPHID = %s AND milestoneNumber IS NULL',
+ $this->getPHID());
+ } else {
+ $desired_parent = $this;
+ $children = $query_children->loadAllWhere(
+ 'parentProjectPHID = %s',
+ $this->getPHID());
+ }
+
+ $desired_parent_phid = null;
+ if ($desired_parent) {
+ $desired_parent_phid = $desired_parent->getPHID();
+ }
+
+ // Eventually bubble up my direct children. Update the others.
+ foreach ($children as $child) {
+ $child->attachParentProject($desired_parent);
+ $child->setParentProjectPHID($desired_parent_phid);
+ $child->setProjectPathKey(null); // Force a new path key and depth.
+ $child->save();
+
+ // Descend the tree.
+ $child->onDestroyTouchChildren(false);
+ }
+ }
+
/* -( PhabricatorFulltextInterface )--------------------------------------- */
diff --git a/src/docs/user/userguide/projects.diviner b/src/docs/user/userguide/projects.diviner
--- a/src/docs/user/userguide/projects.diviner
+++ b/src/docs/user/userguide/projects.diviner
@@ -337,3 +337,116 @@
Form customization also provides a powerful tool for making many policy
management tasks easier (see @{article:User Guide: Customizing Forms}).
+
+Archiving
+=========
+
+In Phorge, you can both destroy a project (dangerous) or archive it (very safe).
+
+Archiving a project (or a milestone, which is a kind of project)
+is very much recommended, for example, to archive a project when it stops
+being useful, when the project reaches its deadline, or when its investor turns
+out to be a scam. You might be surprised how useful it is to know which
+colleagues have worked on a certain very old archived project, which fortunately
+somebody decided to archive rather than fully destroy.
+
+The {nav icon=ban,name=Archive} action is visible to all people who
+can {nav icon=pencil,name=Edit} a project. As usual in Phorge,
+there is a confirmation dialog.
+
+After you confirm the archiving, these things will happen:
+
+- wherever the tag or its hashtag are mentioned, the corresponding badge is
+ appropriately de-colorized or struck-through.
+- the archived project stops appearing in the active projects list at
+ [ /project/query/active/ ]( /project/query/active/ )
+- the archived project is de-prioritized from most search results and selectors,
+ including the top search bar, the tag pickers, etc.
+- the archived project is muted, and does not cause "watch" notifications.
+- the performer of this action is logged in the recent actions.
+
+All these consequences are reversible. You can bring a project back
+to life anytime using the {nav icon=check,name=Activate project} action.
+
+After archiving a project, all tagged objects, tagged tasks, etc. will be
+intentionally kept as-is. In particular, no special read-only policy is
+enforced on these tagged objects. This is in line with the above section
+about "Policies In Depth". In fact, an object can have many tags, and
+if a specific team group ceases its operations, that does not mean that
+others should stop working on the same tasks, etc.
+
+In case additional hiding of information is needed, you can reduce the
+visibility policy of that project. For example, the visibility policy
+"No One" makes the project effectively invisible to others.
+Mastering the visibility policies helps a lot in making sure your cleanup
+requests are managed professionally and in a secure way, still allowing
+future auditing, when needed.
+
+If you still decide against archiving a project in a recoverable way,
+continue to the following scary and dangerous section about
+permanent destruction.
+
+Permanently Destroying
+======================
+
+Phorge is designed as a safe collaborative platform that rarely
+requires @{article:Permanently Destroying Data}.
+
+If you have read that article, and if you have done a backup, and if
+you have access to the command line, and if you still want to permanently
+destroy a project (or a milestone), these will be the consequences:
+
+- the project is destroyed permanently from the database, forever
+ (unless you have a good backup and sufficient recovery skills)
+- all objects, including tasks, repositories, wiki documents, calendars,
+ secrets, other projects, etc. to which you have set visibility restrictions
+ involving that project (example: "visible to project members")
+ will be broken, and everyone will be locked out of them.
+ That means these objects will become completely invisible from the web
+ interface and API results.
+ You can still recover from these particular policy problems,
+ by reading the @{article:User Guide: Unlocking Objects}.
+- users that were members or watchers will lose that relationship.
+- tagged items will lose that relationship, including tasks, commits,
+ wiki documents, calendar events, repositories, etc.; these objects
+ will simply not be associated anymore with that project tag
+ (but will remain associated with other tags, of course).
+- comments and texts mentioning the hashtag of that `#project` will no longer
+ render that project link.
+ You will still be able to add that hashtag to another project,
+ to revive these links.
+- if the project has a workboard, that workboard will be destroyed as well
+ (tasks in that workboard will always be kept and will remain associated
+ with other workboards).
+- if the project has direct milestones, these milestones will be destroyed
+ as well
+ (recall that milestones are technically projects, so consider reading this
+ list again to understand what will happen to these milestones, and to items
+ associated to them, etc.)
+- if the project has a parent project, and if that parent will end up with
+ no other child projects, that parent can be promoted to root-project again.
+ This means membership of the parent project will be editable again.
+- if the project has subprojects, all subprojects and all their descendant
+ subprojects will climb the tree depth by one level, to fill the gap
+ you caused. Grandchildren become children, children become parents,
+ etc. - a real mess for family photos.
+- you increase the risk of something completely unexpected happening,
+ such as the destruction of your entire datacenter by our Slugma Pokemons
+ out of recursion control.
+
+To permanently destroy a project, you will need to execute a command like this,
+from the root directory of the Phorge repository on your server:
+
+```
+./bin/remove destroy PHID-PROJ-abcdef123456
+```
+
+The command needs the "PHID" code of the project.
+Every project has a PHID which can be retrieved in multiple ways,
+including the {nav icon=cog,name=Manage} menu of that project, or hovering
+the cursor on the {nav icon=flag,name=Flag For Later} feature.
+
+This command requires a manual confirmation. Before proceeding,
+take the disclaimer seriously and read again the previous section about
+archiving projects (safe), instead of permanently destroying them (unsafe),
+to hopefully change your mind.
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, May 8, 10:43 (3 d, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1498574
Default Alt Text
D25772.1746701035.diff (12 KB)
Attached To
Mode
D25772: Projects: improve quality of destroy workflow
Attached
Detach File
Event Timeline
Log In to Comment