Page MenuHomePhorge

D25772.1746861281.diff
No OneTemporary

D25772.1746861281.diff

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,75 @@
$slug->delete();
}
+ // Destroy my milestones because they cannot live without me.
+ // Do not use PhabricatorProjectQuery to avoid a circular dependency,
+ // and, to do not have a load 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 holes in the tree.
+ $this->onDestroyTouchChildren(true);
+
+ // After the tree is fixed, update my parent 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.
+ // We must skip my direct milestones since they are under removal.
+ // Do not use PhabricatorProjectQuery to avoid a circular dependency.
+ $query_children = new self();
+ if ($close_my_hole) {
+ $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());
+ }
+
+ // The desired parent PHID for my children may become NULL,
+ // when we are closing my hole but my parent it's a root-project.
+ $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,125 @@
Form customization also provides a powerful tool for making many policy
management tasks easier (see @{article:User Guide: Customizing Forms}).
+
+Archiving
+=========
+
+Phorge supports the destruction (unsafe) and archival (safe) of projects.
+
+Archiving a project (or archiving a milestone - since milestones are projects)
+is very much recommended: you are encouraged 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 violently destroy.
+Archiving even makes sense when your boss creates a nonsense project about
+integrating Phorge with Doom, but would like to hide all traces...
+Please resist looking for destruction, and evaluate archiving instead.
+
+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, these things will happen:
+
+- in general, the archived project will avoid distracting people,
+ still preserving its past glory.
+- whatever mentions the tag or its hashtag, the related badge is generously
+ de-colorized or struck-through.
+- the archived project is unlisted from the active 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.
+- who triggered 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 tagged objects. This should be familiar if you have read
+"Policies In Depth". In short, an object can have many tags, and if a
+specific team group closes its operations, that does not mean that
+others should stop working on all their tagged tasks, etc.
+
+After you present "how amazing is Phorge about archiving projects" and
+after you distributed stickers about `#JustArchive` to all coworkers,
+it's a classic that somebody still want to "just remove the project" or
+"make it go away" or "run obliviate", etc.
+In these cases, where "more censorship" is needed, you can evaluate a
+change to the visibility settings of that project. For example, the very
+limited visibility "show only to me" 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.
+
+At this point, if you still haven't convinced everyone to archive a specific
+project, explore the next scary and unsafe section about permanently destroying.
+
+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 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 recover skills)
+- all objects, including tasks, repositories, wiki documents, calendars,
+ secrets, other projects, etc. to which you set visibility restrictions
+ involving that project (example: "visible to project members")
+ will be broken, and everyone will be locked out of them.
+ It 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}.
+- tagged items are generally preserved, including tasks, commits,
+ wiki documents, calendar events, repositories, etc. and these objects
+ will simply not be associated anymore with that project tag
+ (but will remain associated with other tags, of course).
+- users that are members or watchers of the destroyed project will be
+ kept in your Phorge but unassigned from that project.
+ Watchers might go crazy until they find something else to watch.
+- comments and texts wrote by users will be preserved even if they were
+ mentioning your `#project` but that hashtag will not render a link.
+ You will still be able to add that hashtag in another project,
+ to revive these links.
+- if the project has a workboard, that workboard is 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 are destroyed as well
+ (note that milestones are technically projects, so, read this list
+ again aloud to understand what will happen to these milestones, and to items
+ associated to these milestones, etc.)
+- if the project has a parent project, and if that parent has no other child
+ projects anymore, that parent can be promoted to root-project again.
+ This means membership of the parent project will be editable again.
+- if the project has sub-projects, all sub-projects and all their descendant
+ sub-projects will climb the tree depth by one level, to fill the hole
+ you caused. Grandchildren become children, sons 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 and it can be easily retrieved in multiple ways,
+including the {nav icon=cog,name=Manage} menu of that project, 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 eventually change your mind.

File Metadata

Mime Type
text/plain
Expires
Sat, May 10, 07:14 (17 m, 9 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1498576
Default Alt Text
D25772.1746861281.diff (13 KB)

Event Timeline